Layout refresh latency bug revised
This commit is contained in:
+19
-3
@@ -1,6 +1,6 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Description: Flask backend API server for authentication, project management, PDK library access, layout preview, and GDS build endpoints.
|
# Description: Flask backend API server for authentication, project management, PDK library access, layout preview, and GDS build endpoints.
|
||||||
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
|
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, file_version, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
|
||||||
# Developer : Qin Yue @ 2026
|
# Developer : Qin Yue @ 2026
|
||||||
# Organization : OptiHK Limited
|
# Organization : OptiHK Limited
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -9,6 +9,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
import yaml
|
import yaml
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -135,6 +136,12 @@ def cell_svg_path(project_name, cell_name):
|
|||||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.svg")
|
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.svg")
|
||||||
|
|
||||||
|
|
||||||
|
def file_version(path):
|
||||||
|
"""Return a cache-busting version token for a completed file."""
|
||||||
|
stat = os.stat(path)
|
||||||
|
return f"{stat.st_mtime_ns}-{stat.st_size}"
|
||||||
|
|
||||||
|
|
||||||
def cell_routes_path(project_name, cell_name):
|
def cell_routes_path(project_name, cell_name):
|
||||||
"""Return the route sidecar JSON path for a project cell."""
|
"""Return the route sidecar JSON path for a project cell."""
|
||||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.routes.yml")
|
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.routes.yml")
|
||||||
@@ -739,24 +746,31 @@ def save_layout():
|
|||||||
write_route_points_sidecar(content, cell_routes_path(project, cell))
|
write_route_points_sidecar(content, cell_routes_path(project, cell))
|
||||||
|
|
||||||
svg_path = None
|
svg_path = None
|
||||||
|
svg_version = None
|
||||||
preview_status = "not_requested"
|
preview_status = "not_requested"
|
||||||
preview_error = None
|
preview_error = None
|
||||||
if create_preview:
|
if create_preview:
|
||||||
svg_path = cell_svg_path(project, cell)
|
svg_path = cell_svg_path(project, cell)
|
||||||
|
temp_svg_path = f"{svg_path}.building-{os.getpid()}-{uuid.uuid4().hex}.svg"
|
||||||
try:
|
try:
|
||||||
create_routed_layout_svg(
|
create_routed_layout_svg(
|
||||||
content,
|
content,
|
||||||
svg_path,
|
temp_svg_path,
|
||||||
pdk_root=current_pdk_root(),
|
pdk_root=current_pdk_root(),
|
||||||
project_dir=project_root(project),
|
project_dir=project_root(project),
|
||||||
technology_manifest_path=technology_manifest_path_for_project(project),
|
technology_manifest_path=technology_manifest_path_for_project(project),
|
||||||
prefer_full_gds=prefer_full_gds_for_session(session),
|
prefer_full_gds=prefer_full_gds_for_session(session),
|
||||||
)
|
)
|
||||||
|
os.replace(temp_svg_path, svg_path)
|
||||||
|
svg_version = file_version(svg_path)
|
||||||
preview_status = "generated"
|
preview_status = "generated"
|
||||||
except RouterStackUnavailable as e:
|
except RouterStackUnavailable as e:
|
||||||
preview_status = "skipped"
|
preview_status = "skipped"
|
||||||
preview_error = str(e)
|
preview_error = str(e)
|
||||||
svg_path = None
|
svg_path = None
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_svg_path):
|
||||||
|
os.remove(temp_svg_path)
|
||||||
|
|
||||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -765,7 +779,9 @@ def save_layout():
|
|||||||
"cell": cell,
|
"cell": cell,
|
||||||
"path": save_path,
|
"path": save_path,
|
||||||
"svg_path": svg_path,
|
"svg_path": svg_path,
|
||||||
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None,
|
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version) if svg_path else None,
|
||||||
|
"svg_ready": bool(svg_path and svg_version),
|
||||||
|
"svg_version": svg_version,
|
||||||
"preview_status": preview_status,
|
"preview_status": preview_status,
|
||||||
"preview_error": preview_error
|
"preview_error": preview_error
|
||||||
}), 200
|
}), 200
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 319 KiB |
@@ -0,0 +1,211 @@
|
|||||||
|
# =============================================
|
||||||
|
# mxPIC Cell/Project Definition File
|
||||||
|
# =============================================
|
||||||
|
schema_version: "2.0.0"
|
||||||
|
kind: cell
|
||||||
|
coordinate_system: gds_y_up
|
||||||
|
canvas_size:
|
||||||
|
width: 500
|
||||||
|
height: 600
|
||||||
|
project: mxpic_project_1
|
||||||
|
name: canvas_1
|
||||||
|
type: composite
|
||||||
|
version: "1.0.0"
|
||||||
|
|
||||||
|
# 1. External Ports (How this cell connects to the outside world)
|
||||||
|
pins:
|
||||||
|
- name: port_io1
|
||||||
|
layer: WG_CORE
|
||||||
|
element: port
|
||||||
|
pin: io1
|
||||||
|
x: 40.0
|
||||||
|
y: -90.0
|
||||||
|
angle: 180.0
|
||||||
|
width: 0.5
|
||||||
|
- name: port_1_io1
|
||||||
|
layer: WG_CORE
|
||||||
|
element: port_1
|
||||||
|
pin: io1
|
||||||
|
x: 410.0
|
||||||
|
y: -35.0
|
||||||
|
angle: 0.0
|
||||||
|
width: 0.5
|
||||||
|
- name: port_1_io2
|
||||||
|
layer: WG_CORE
|
||||||
|
element: port_1
|
||||||
|
pin: io2
|
||||||
|
x: 410.0
|
||||||
|
y: -25.0
|
||||||
|
angle: 0.0
|
||||||
|
width: 0.5
|
||||||
|
- name: port_2_io1
|
||||||
|
layer: WG_CORE
|
||||||
|
element: port_2
|
||||||
|
pin: io1
|
||||||
|
x: 390.0
|
||||||
|
y: -215.0
|
||||||
|
angle: 0.0
|
||||||
|
width: 0.5
|
||||||
|
- name: port_2_io2
|
||||||
|
layer: WG_CORE
|
||||||
|
element: port_2
|
||||||
|
pin: io2
|
||||||
|
x: 390.0
|
||||||
|
y: -205.0
|
||||||
|
angle: 0.0
|
||||||
|
width: 0.5
|
||||||
|
|
||||||
|
# 2. Instances (The sub-components dropped onto this canvas)
|
||||||
|
instances:
|
||||||
|
MMI_1:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||||
|
x: 130.0
|
||||||
|
y: -90.0
|
||||||
|
rotation: 0.0
|
||||||
|
flip: 0
|
||||||
|
flop: 0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
MMI_2:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||||
|
x: 280.0
|
||||||
|
y: -30.0
|
||||||
|
rotation: 0.0
|
||||||
|
flip: 0
|
||||||
|
flop: 0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
MMI_3:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||||
|
x: 320.1
|
||||||
|
y: -144.7
|
||||||
|
rotation: 0.0
|
||||||
|
flip: 0
|
||||||
|
flop: 0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
elements:
|
||||||
|
port:
|
||||||
|
type: port
|
||||||
|
x: 40.0
|
||||||
|
y: -90.0
|
||||||
|
angle: 180.0
|
||||||
|
pin_number: 1
|
||||||
|
pitch: 10
|
||||||
|
layer: WG_CORE
|
||||||
|
width: 0.5
|
||||||
|
description: ""
|
||||||
|
pins:
|
||||||
|
- name: port_io1
|
||||||
|
role: io1
|
||||||
|
port:
|
||||||
|
type: port
|
||||||
|
x: 40.0
|
||||||
|
y: -90.0
|
||||||
|
angle: 0.0
|
||||||
|
pin_number: 1
|
||||||
|
pitch: 10
|
||||||
|
layer: WG_CORE
|
||||||
|
width: 0.5
|
||||||
|
description: ""
|
||||||
|
pins:
|
||||||
|
- name: port_io1
|
||||||
|
role: io1
|
||||||
|
port_1:
|
||||||
|
type: port
|
||||||
|
x: 410.0
|
||||||
|
y: -30.0
|
||||||
|
angle: 180.0
|
||||||
|
pin_number: 2
|
||||||
|
pitch: 10
|
||||||
|
layer: WG_CORE
|
||||||
|
width: 0.5
|
||||||
|
description: ""
|
||||||
|
pins:
|
||||||
|
- name: port_1_io1
|
||||||
|
role: io1
|
||||||
|
- name: port_1_io2
|
||||||
|
role: io2
|
||||||
|
port_2:
|
||||||
|
type: port
|
||||||
|
x: 390.0
|
||||||
|
y: -210.0
|
||||||
|
angle: 180.0
|
||||||
|
pin_number: 2
|
||||||
|
pitch: 10
|
||||||
|
layer: WG_CORE
|
||||||
|
width: 0.5
|
||||||
|
description: ""
|
||||||
|
pins:
|
||||||
|
- name: port_2_io1
|
||||||
|
role: io1
|
||||||
|
- name: port_2_io2
|
||||||
|
role: io2
|
||||||
|
|
||||||
|
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||||
|
bundles:
|
||||||
|
output_bus:
|
||||||
|
routing_type: euler_bend
|
||||||
|
links:
|
||||||
|
- from: MMI_1:a1
|
||||||
|
to: port:port_io1
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_2:a1
|
||||||
|
to: MMI_1:b1
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_3:a1
|
||||||
|
to: MMI_1:b2
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_2:b1
|
||||||
|
to: port_1:port_1_io1
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_2:b1
|
||||||
|
to: port_1:port_1_io2
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_2:b2
|
||||||
|
to: port_1:port_1_io1
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_3:b1
|
||||||
|
to: port_2:port_2_io2
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: MMI_3:b2
|
||||||
|
to: port_2:port_2_io1
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
Binary file not shown.
+24
-7
@@ -3760,6 +3760,8 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
const canvasViewportRef = useRef(null);
|
const canvasViewportRef = useRef(null);
|
||||||
|
const buildLayoutRequestRef = useRef(0);
|
||||||
|
const buildLayoutBusyRef = useRef(false);
|
||||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||||
|
|
||||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||||
@@ -6208,49 +6210,64 @@ ${bundlesBlock}`;
|
|||||||
// Save the active page, generate layout preview assets, and show the preview tab.
|
// Save the active page, generate layout preview assets, and show the preview tab.
|
||||||
const handleBuildLayout = useCallback(async () => {
|
const handleBuildLayout = useCallback(async () => {
|
||||||
if (!activePage) return;
|
if (!activePage) return;
|
||||||
if (buildLayoutBusy) return;
|
if (buildLayoutBusyRef.current) return;
|
||||||
if (!validateRouteCrossings(activePage)) return;
|
if (!validateRouteCrossings(activePage)) return;
|
||||||
|
const buildPage = activePage;
|
||||||
|
const buildRequestId = buildLayoutRequestRef.current + 1;
|
||||||
|
buildLayoutRequestRef.current = buildRequestId;
|
||||||
|
buildLayoutBusyRef.current = true;
|
||||||
setBuildLayoutBusy(true);
|
setBuildLayoutBusy(true);
|
||||||
startBuildProgress('Building layout');
|
startBuildProgress('Building layout');
|
||||||
const yamlContent = buildYamlForPage(activePage);
|
const yamlContent = buildYamlForPage(buildPage);
|
||||||
|
const layoutBounds = calculateLayoutBounds(buildPage);
|
||||||
|
|
||||||
// send to backend
|
// send to backend
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/save-layout', {
|
const response = await fetch('/api/save-layout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
cache: 'no-store',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
project: currentProjectName,
|
project: currentProjectName,
|
||||||
cell: activePage.name,
|
cell: buildPage.name,
|
||||||
content: yamlContent,
|
content: yamlContent,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errData = await response.json();
|
const errData = await response.json().catch(() => ({}));
|
||||||
|
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||||
addLog(errData.error || 'Save failed, unknown error');
|
addLog(errData.error || 'Save failed, unknown error');
|
||||||
stopBuildProgress();
|
stopBuildProgress();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||||
addLog('Successfully saved: ' + result.path);
|
addLog('Successfully saved: ' + result.path);
|
||||||
if (result.preview_error) {
|
if (result.preview_error) {
|
||||||
addLog('Preview skipped: ' + result.preview_error);
|
addLog('Preview skipped: ' + result.preview_error);
|
||||||
}
|
}
|
||||||
if (result.svg_url) {
|
if (result.svg_ready && result.svg_url) {
|
||||||
completeBuildProgress('Layout ready');
|
completeBuildProgress('Layout ready');
|
||||||
openLayoutPreview(activePage.name, result.svg_url, calculateLayoutBounds(activePage));
|
openLayoutPreview(buildPage.name, result.svg_url, layoutBounds);
|
||||||
} else {
|
} else {
|
||||||
|
if (result.preview_status === 'generated') {
|
||||||
|
addLog('Layout SVG was not marked ready by the backend.');
|
||||||
|
}
|
||||||
completeBuildProgress('Layout saved');
|
completeBuildProgress('Layout saved');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||||
addLog('Save error: ' + err.message);
|
addLog('Save error: ' + err.message);
|
||||||
stopBuildProgress();
|
stopBuildProgress();
|
||||||
} finally {
|
} finally {
|
||||||
|
if (buildRequestId === buildLayoutRequestRef.current) {
|
||||||
|
buildLayoutBusyRef.current = false;
|
||||||
setBuildLayoutBusy(false);
|
setBuildLayoutBusy(false);
|
||||||
}
|
}
|
||||||
}, [activePage, buildLayoutBusy, buildYamlForPage, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings, startBuildProgress, completeBuildProgress, stopBuildProgress]);
|
}
|
||||||
|
}, [activePage, buildYamlForPage, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings, startBuildProgress, completeBuildProgress, stopBuildProgress]);
|
||||||
|
|
||||||
// Save YAML for every editable project/composite page without opening previews.
|
// Save YAML for every editable project/composite page without opening previews.
|
||||||
const handleSaveProjectLayouts = useCallback(async () => {
|
const handleSaveProjectLayouts = useCallback(async () => {
|
||||||
|
|||||||
@@ -60,6 +60,18 @@ assert(
|
|||||||
serverPy.includes('svg_url'),
|
serverPy.includes('svg_url'),
|
||||||
'save-layout response should include an svg_url for the new layout tab'
|
'save-layout response should include an svg_url for the new layout tab'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('svg_ready') &&
|
||||||
|
serverPy.includes('svg_version') &&
|
||||||
|
serverPy.includes('file_version(svg_path)') &&
|
||||||
|
serverPy.includes("url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version)"),
|
||||||
|
'save-layout response should only expose a versioned SVG URL after the preview file is ready'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('temp_svg_path') &&
|
||||||
|
serverPy.includes('os.replace(temp_svg_path, svg_path)'),
|
||||||
|
'save-layout should publish generated SVG previews atomically instead of serving partially written files'
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
serverPy.includes('RouterStackUnavailable') &&
|
serverPy.includes('RouterStackUnavailable') &&
|
||||||
serverPy.includes('except RouterStackUnavailable as e') &&
|
serverPy.includes('except RouterStackUnavailable as e') &&
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ assert(
|
|||||||
canvasHtml.includes('svg_url'),
|
canvasHtml.includes('svg_url'),
|
||||||
'Build Layout should use the backend svg_url response'
|
'Build Layout should use the backend svg_url response'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('result.svg_ready && result.svg_url') &&
|
||||||
|
canvasHtml.includes('buildLayoutRequestRef') &&
|
||||||
|
canvasHtml.includes('buildLayoutBusyRef') &&
|
||||||
|
canvasHtml.includes("cache: 'no-store'"),
|
||||||
|
'Build Layout should wait for a ready, versioned SVG response and prevent stale duplicate preview updates'
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
canvasHtml.includes('result.preview_error') &&
|
canvasHtml.includes('result.preview_error') &&
|
||||||
canvasHtml.includes('Preview skipped: '),
|
canvasHtml.includes('Preview skipped: '),
|
||||||
|
|||||||
Reference in New Issue
Block a user