From 7ac76aaee9cca889566ec06702888be400258f56 Mon Sep 17 00:00:00 2001
From: = <=>
Date: Thu, 4 Jun 2026 19:47:18 +0800
Subject: [PATCH] Layout refresh latency bug revised
---
backend/server.py | 22 +-
.../admin/layout/mxpic_project_1/canvas_1.svg | 293 ++++++++++++++++++
.../admin/layout/mxpic_project_1/canvas_1.yml | 211 +++++++++++++
database/mxpic_data.db | Bin 131072 -> 135168 bytes
frontend/canvas.html | 33 +-
tests/layout-backend-static.test.js | 12 +
tests/layout-ui-wiring.test.js | 7 +
7 files changed, 567 insertions(+), 11 deletions(-)
create mode 100644 database/admin/layout/mxpic_project_1/canvas_1.svg
create mode 100644 database/admin/layout/mxpic_project_1/canvas_1.yml
diff --git a/backend/server.py b/backend/server.py
index 1d422d7..7b3d2f9 100644
--- a/backend/server.py
+++ b/backend/server.py
@@ -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
diff --git a/database/admin/layout/mxpic_project_1/canvas_1.svg b/database/admin/layout/mxpic_project_1/canvas_1.svg
new file mode 100644
index 0000000..a416194
--- /dev/null
+++ b/database/admin/layout/mxpic_project_1/canvas_1.svg
@@ -0,0 +1,293 @@
+
+
\ No newline at end of file
diff --git a/database/admin/layout/mxpic_project_1/canvas_1.yml b/database/admin/layout/mxpic_project_1/canvas_1.yml
new file mode 100644
index 0000000..17d83e6
--- /dev/null
+++ b/database/admin/layout/mxpic_project_1/canvas_1.yml
@@ -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
\ No newline at end of file
diff --git a/database/mxpic_data.db b/database/mxpic_data.db
index 817af6726d4b6581ea2c659055c43680e76f86f9..f97bbbd668296c55a9df94445b0f1f297956087e 100644
GIT binary patch
delta 1191
zcmZo@;AmLDF+rMDoq>TtaiW4fqx!~#DfSZUI2oD68B2>(i;9_8fl|`^NkEc8aWapC
z2^W77BR_*XL!+zwWX6?yHnTa#@Ua0U6hEV~nQj-OfourXOO@LawS|z!G7AR|LH<
zUSs5KbZ*sy7$gfcNU+41*~HLf`kqgWO4CKYF`7@l&?*bJ+Q`_-z$^-=LzfgCh9Dg}
zMC&lJG6mbAjiQ6a*wVyk`rc2BikQwaG_o=_09m3%iWWmlkQPniwHR6%fo;(sMT?;+
z$QE_twHR2L7=yH^k)p*I9Ac`ZXfXmiOobFJkPuTQT8n{&m8k{DFG?h70SB@oDOy1J
z!MRm|6fMT!*q0|sixJ2cITS6-z+{OdvjDTUF*uB6QFMS3vN>u(=7VQ#b1Ng`C;$s!
BPWk`<
delta 102
zcmZozz|qjaF+rMDje&tdVWNUPquR!VDfSXGIT@M78B2>(i;9_8fl^ZZ89 ({ 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 {
- setBuildLayoutBusy(false);
+ 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 () => {
diff --git a/tests/layout-backend-static.test.js b/tests/layout-backend-static.test.js
index 9721715..2fd64fa 100644
--- a/tests/layout-backend-static.test.js
+++ b/tests/layout-backend-static.test.js
@@ -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') &&
diff --git a/tests/layout-ui-wiring.test.js b/tests/layout-ui-wiring.test.js
index 1cec144..4552f06 100644
--- a/tests/layout-ui-wiring.test.js
+++ b/tests/layout-ui-wiring.test.js
@@ -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: '),