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.
|
||||
# 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
|
||||
# Organization : OptiHK Limited
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import json
|
||||
import uuid
|
||||
import yaml
|
||||
from collections import OrderedDict
|
||||
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")
|
||||
|
||||
|
||||
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):
|
||||
"""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")
|
||||
@@ -739,24 +746,31 @@ def save_layout():
|
||||
write_route_points_sidecar(content, cell_routes_path(project, cell))
|
||||
|
||||
svg_path = None
|
||||
svg_version = None
|
||||
preview_status = "not_requested"
|
||||
preview_error = None
|
||||
if create_preview:
|
||||
svg_path = cell_svg_path(project, cell)
|
||||
temp_svg_path = f"{svg_path}.building-{os.getpid()}-{uuid.uuid4().hex}.svg"
|
||||
try:
|
||||
create_routed_layout_svg(
|
||||
content,
|
||||
svg_path,
|
||||
temp_svg_path,
|
||||
pdk_root=current_pdk_root(),
|
||||
project_dir=project_root(project),
|
||||
technology_manifest_path=technology_manifest_path_for_project(project),
|
||||
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"
|
||||
except RouterStackUnavailable as e:
|
||||
preview_status = "skipped"
|
||||
preview_error = str(e)
|
||||
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})
|
||||
return jsonify({
|
||||
@@ -765,7 +779,9 @@ def save_layout():
|
||||
"cell": cell,
|
||||
"path": save_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_error": preview_error
|
||||
}), 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 canvasViewportRef = useRef(null);
|
||||
const buildLayoutRequestRef = useRef(0);
|
||||
const buildLayoutBusyRef = useRef(false);
|
||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||
|
||||
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.
|
||||
const handleBuildLayout = useCallback(async () => {
|
||||
if (!activePage) return;
|
||||
if (buildLayoutBusy) return;
|
||||
if (buildLayoutBusyRef.current) return;
|
||||
if (!validateRouteCrossings(activePage)) return;
|
||||
const buildPage = activePage;
|
||||
const buildRequestId = buildLayoutRequestRef.current + 1;
|
||||
buildLayoutRequestRef.current = buildRequestId;
|
||||
buildLayoutBusyRef.current = true;
|
||||
setBuildLayoutBusy(true);
|
||||
startBuildProgress('Building layout');
|
||||
const yamlContent = buildYamlForPage(activePage);
|
||||
const yamlContent = buildYamlForPage(buildPage);
|
||||
const layoutBounds = calculateLayoutBounds(buildPage);
|
||||
|
||||
// send to backend
|
||||
try {
|
||||
const response = await fetch('/api/save-layout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
project: currentProjectName,
|
||||
cell: activePage.name,
|
||||
cell: buildPage.name,
|
||||
content: yamlContent,
|
||||
}),
|
||||
});
|
||||
|
||||
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');
|
||||
stopBuildProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
addLog('Successfully saved: ' + result.path);
|
||||
if (result.preview_error) {
|
||||
addLog('Preview skipped: ' + result.preview_error);
|
||||
}
|
||||
if (result.svg_url) {
|
||||
if (result.svg_ready && result.svg_url) {
|
||||
completeBuildProgress('Layout ready');
|
||||
openLayoutPreview(activePage.name, result.svg_url, calculateLayoutBounds(activePage));
|
||||
openLayoutPreview(buildPage.name, result.svg_url, layoutBounds);
|
||||
} else {
|
||||
if (result.preview_status === 'generated') {
|
||||
addLog('Layout SVG was not marked ready by the backend.');
|
||||
}
|
||||
completeBuildProgress('Layout saved');
|
||||
}
|
||||
} catch (err) {
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
addLog('Save error: ' + err.message);
|
||||
stopBuildProgress();
|
||||
} finally {
|
||||
if (buildRequestId === buildLayoutRequestRef.current) {
|
||||
buildLayoutBusyRef.current = 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.
|
||||
const handleSaveProjectLayouts = useCallback(async () => {
|
||||
|
||||
@@ -60,6 +60,18 @@ assert(
|
||||
serverPy.includes('svg_url'),
|
||||
'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(
|
||||
serverPy.includes('RouterStackUnavailable') &&
|
||||
serverPy.includes('except RouterStackUnavailable as e') &&
|
||||
|
||||
@@ -32,6 +32,13 @@ assert(
|
||||
canvasHtml.includes('svg_url'),
|
||||
'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(
|
||||
canvasHtml.includes('result.preview_error') &&
|
||||
canvasHtml.includes('Preview skipped: '),
|
||||
|
||||
Reference in New Issue
Block a user