perf: demand-load mobile workspace features (Closes #1468)
Some checks failed
CI / lint (pull_request) Successful in 3m49s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Failing after 9m26s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-27 11:39:32 +00:00
parent 2a44cf20fc
commit 5d9cec1dd7
7 changed files with 204 additions and 40 deletions

View File

@ -8614,4 +8614,5 @@
function widgetTick() { const el=qs('#widget-clock'); if(el) el.textContent = fmt(new Date()); } function widgetTick() { const el=qs('#widget-clock'); if(el) el.textContent = fmt(new Date()); }
setInterval(widgetTick, 1000); setInterval(widgetTick, 1000);
workspaceLifecycle.markWorkspaceReady?.();
})(); })();

View File

@ -301,6 +301,8 @@ const SHELL = [
]; ];
const OPTIONAL_FEATURES = [ const OPTIONAL_FEATURES = [
]; ];
const DEMAND_FEATURES = [
];
const SHARED_IMAGE_ID = 'shared-image'; const SHARED_IMAGE_ID = 'shared-image';
const SHARED_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']); const SHARED_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
@ -1027,7 +1029,8 @@ self.addEventListener('fetch', event => {
event.respondWith(caches.match(request).then(cached => cached || fetch(request))); event.respondWith(caches.match(request).then(cached => cached || fetch(request)));
return; return;
} }
if (url.origin === self.location.origin && OPTIONAL_FEATURES.includes(url.pathname)) { if (url.origin === self.location.origin &&
(OPTIONAL_FEATURES.includes(url.pathname) || DEMAND_FEATURES.includes(url.pathname))) {
event.respondWith(cachedOptionalFeature(request)); event.respondWith(cachedOptionalFeature(request));
} }
}); });

View File

@ -19,6 +19,15 @@ async function loadWorkspace({
const failed = new Set(); const failed = new Set();
const recoveries = new Map(); const recoveries = new Map();
let retryInFlight = null; let retryInFlight = null;
let resolveWorkspaceReady;
const workspaceReady = new Promise(resolve => { resolveWorkspaceReady = resolve; });
let workspaceMarkedReady = false;
const markWorkspaceReady = () => {
if (workspaceMarkedReady) return false;
workspaceMarkedReady = true;
resolveWorkspaceReady(true);
return true;
};
const loadFeature = name => { const loadFeature = name => {
attempts[name] += 1; attempts[name] += 1;
@ -73,23 +82,55 @@ async function loadWorkspace({
} }
hideRecovery(); hideRecovery();
const optional = ['today-timer', 'planning'].map(async name => { let optionalReady = null;
try { const hydrateWorkspace = () => {
await retryOnce(name); if (optionalReady) return optionalReady;
return true; let resolveOptional;
} catch (_error) { optionalReady = new Promise(resolve => { resolveOptional = resolve; });
failed.add(name); const optional = ['today-timer', 'planning'].map(async name => {
showRecovery(); try {
return new Promise(resolve => recoveries.set(name, {resolve})); await retryOnce(name);
} return true;
}); } catch (_error) {
const optionalReady = Promise.all(optional).then(() => { failed.add(name);
hideRecovery(); showRecovery();
return true; return new Promise(resolve => recoveries.set(name, {resolve}));
}); }
});
Promise.all(optional).then(() => {
hideRecovery();
resolveOptional(true);
});
return optionalReady;
};
const hydrationSelector = [
'[data-mobile-task]:not([data-mobile-task="work"]):not([data-mobile-task="queues"])',
'[data-progressive-loading="true"]',
'#app-menu-toggle',
'#work-settings-toggle',
].join(',');
const hydrateForAction = async event => {
const target = event.target?.closest?.(hydrationSelector);
if (!target) return;
event.preventDefault();
event.stopImmediatePropagation();
await hydrateWorkspace();
await workspaceReady;
document.removeEventListener?.('click', hydrateForAction, true);
target.click?.();
};
document.addEventListener?.('click', hydrateForAction, true);
const hash = window?.location?.hash || '';
const deepLinkReady = hash.startsWith('#/') && hash !== '#/my-work' ?
hydrateWorkspace() : Promise.resolve(false);
return { return {
optionalReady, hydrateWorkspace,
deepLinkReady,
workspaceReady,
markWorkspaceReady,
get optionalReady() { return hydrateWorkspace(); },
retryFeature(name) { retryFeature(name) {
if (!failed.has(name)) return Promise.resolve(true); if (!failed.has(name)) return Promise.resolve(true);
return retryFailed().then(() => !failed.has(name)); return retryFailed().then(() => !failed.has(name));

View File

@ -140,7 +140,16 @@ def build_frontend(frontend_dir: Path) -> FrontendBuild:
for source in sources: for source in sources:
if source != WORKER_RUNTIME_SOURCE: if source != WORKER_RUNTIME_SOURCE:
worker = worker.replace(f" BASE + '{source}',\n", "") worker = worker.replace(f" BASE + '{source}',\n", "")
optional_features = feature_bundles # The workspace hydrator fetches these large chunks only when a route or action
# needs them; warming them here would defeat demand loading on every visit.
optional_features = {
name: bundle for name, bundle in feature_bundles.items()
if name not in {"today-timer", "planning"}
}
demand_features = {
name: bundle for name, bundle in feature_bundles.items()
if name in {"today-timer", "planning"}
}
worker = worker.replace( worker = worker.replace(
" BASE + 'static/dashboard.css',\n", " BASE + 'static/dashboard.css',\n",
" BASE + 'static/dashboard.css',\n" " BASE + 'static/dashboard.css',\n"
@ -151,6 +160,11 @@ def build_frontend(frontend_dir: Path) -> FrontendBuild:
"const OPTIONAL_FEATURES = [\n" "const OPTIONAL_FEATURES = [\n"
+ "".join(f" BASE + '{bundle.runtime_name}',\n" for bundle in optional_features.values()), + "".join(f" BASE + '{bundle.runtime_name}',\n" for bundle in optional_features.values()),
) )
worker = worker.replace(
"const DEMAND_FEATURES = [\n",
"const DEMAND_FEATURES = [\n"
+ "".join(f" BASE + '{bundle.runtime_name}',\n" for bundle in demand_features.values()),
)
worker = CACHE_DECLARATION.sub( worker = CACHE_DECLARATION.sub(
"const CACHE = 'stackchain-dashboard-shell-BUILD';", worker, count=1 "const CACHE = 'stackchain-dashboard-shell-BUILD';", worker, count=1
) )

View File

@ -41,6 +41,10 @@ def test_release_artifact_guides_an_empty_mobile_account_to_first_work(
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click() page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle") page.wait_for_url(origin + "/", wait_until="networkidle")
page.evaluate(
"async () => { const lifecycle = await window.stackchainWorkspaceLifecycle; "
"await lifecycle.hydrateWorkspace(); await lifecycle.workspaceReady; }"
)
expect(page.locator("#my-work-status")).to_contain_text("No assigned work") expect(page.locator("#my-work-status")).to_contain_text("No assigned work")
page.locator('[data-mobile-task="work"]').click() page.locator('[data-mobile-task="work"]').click()
@ -100,7 +104,12 @@ def test_release_artifact_guides_an_empty_mobile_account_to_first_work(
fresh_page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) fresh_page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
fresh_page.locator("#submit-sign-in").click() fresh_page.locator("#submit-sign-in").click()
fresh_page.wait_for_url(origin + "/", wait_until="networkidle") fresh_page.wait_for_url(origin + "/", wait_until="networkidle")
fresh_page.evaluate(
"async () => { const lifecycle = await window.stackchainWorkspaceLifecycle; "
"await lifecycle.hydrateWorkspace(); await lifecycle.workspaceReady; }"
)
expect(fresh_page.locator("#my-work-status")).to_contain_text("No assigned work") expect(fresh_page.locator("#my-work-status")).to_contain_text("No assigned work")
fresh_page.wait_for_load_state("networkidle")
assert fresh_page.evaluate( assert fresh_page.evaluate(
"localStorage.getItem('stackchain.first-task.v1:timmy')" "localStorage.getItem('stackchain.first-task.v1:timmy')"
) == "complete" ) == "complete"
@ -128,6 +137,7 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
browser_errors: list[str] = [] browser_errors: list[str] = []
failed_responses: list[str] = [] failed_responses: list[str] = []
workspace_requests: list[str] = [] workspace_requests: list[str] = []
optional_workspace_requests: list[str] = []
launch_transfer_events: list[str] = [] launch_transfer_events: list[str] = []
live_requests: list[str] = [] live_requests: list[str] = []
@ -171,6 +181,12 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
) )
if "feature-work-core-" in request.url else None, if "feature-work-core-" in request.url else None,
) )
page.on(
"request",
lambda request: optional_workspace_requests.append(request.url)
if "feature-today-timer-" in request.url or "feature-planning-" in request.url
else None,
)
page.on( page.on(
"requestfinished", "requestfinished",
lambda request: launch_transfer_events.append("core-finished") lambda request: launch_transfer_events.append("core-finished")
@ -184,6 +200,7 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
) )
page.goto(origin + "/", wait_until="networkidle") page.goto(origin + "/", wait_until="networkidle")
assert optional_workspace_requests == []
page.locator('input[name="device_label"]').fill("Home bootstrap release phone") page.locator('input[name="device_label"]').fill("Home bootstrap release phone")
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
live_requests.clear() live_requests.clear()
@ -194,9 +211,24 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
), launch_transfer_events ), launch_transfer_events
expect(page.locator("#my-work-status")).to_contain_text("2") expect(page.locator("#my-work-status")).to_contain_text("2")
assert optional_workspace_requests == []
revisionless_live_requests = [url for url in live_requests if "?" not in url] revisionless_live_requests = [url for url in live_requests if "?" not in url]
assert len(revisionless_live_requests) == 1, live_requests assert len(revisionless_live_requests) == 1, live_requests
initial_live_requests = len(live_requests) initial_live_requests = len(live_requests)
page.locator("#work-settings-toggle").click()
tomorrow = page.locator("#plan-tomorrow")
expect(tomorrow).to_be_visible()
assert len(optional_workspace_requests) == 2, optional_workspace_requests
tomorrow_bounds = tomorrow.bounding_box()
assert tomorrow_bounds and tomorrow_bounds["height"] >= 44
tomorrow.click()
expect(page.locator("#plan-today-sheet")).to_be_visible()
expect(page.locator("#plan-today-title")).to_have_text("Plan Tomorrow")
expect(page.locator("#save-and-start-today")).to_be_hidden()
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#cancel-plan-today").click()
page.evaluate( page.evaluate(
""" """
() => { () => {
@ -226,18 +258,6 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
bounds = control.bounding_box() bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44 assert bounds and bounds["height"] >= 44
page.locator("#work-settings-toggle").click()
tomorrow = page.locator("#plan-tomorrow")
expect(tomorrow).to_be_visible()
tomorrow_bounds = tomorrow.bounding_box()
assert tomorrow_bounds and tomorrow_bounds["height"] >= 44
tomorrow.click()
expect(page.locator("#plan-today-sheet")).to_be_visible()
expect(page.locator("#plan-today-title")).to_have_text("Plan Tomorrow")
expect(page.locator("#save-and-start-today")).to_be_hidden()
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#cancel-plan-today").click()
page.locator('[data-mobile-task="queues"]').click() page.locator('[data-mobile-task="queues"]').click()
page.locator(".mobile-queue-all summary").click() page.locator(".mobile-queue-all summary").click()
delivery_queue = page.locator('[data-mobile-queue="delivery"]') delivery_queue = page.locator('[data-mobile-queue="delivery"]')
@ -361,6 +381,7 @@ def test_release_artifact_recovers_a_transient_workspace_request_in_place(tmp_pa
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click() page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle") page.wait_for_url(origin + "/", wait_until="networkidle")
page.locator('[data-mobile-task="find"]').click()
page.wait_for_timeout(1500) page.wait_for_timeout(1500)
resources = page.evaluate( resources = page.evaluate(
@ -406,6 +427,10 @@ def test_release_artifact_keeps_mobile_delivery_recovery_single_flight(tmp_path:
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click() page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle") page.wait_for_url(origin + "/", wait_until="networkidle")
page.evaluate(
"async () => { const lifecycle = await window.stackchainWorkspaceLifecycle; "
"await lifecycle.hydrateWorkspace(); await lifecycle.workspaceReady; }"
)
page.evaluate( page.evaluate(
""" """
@ -508,6 +533,10 @@ def test_release_artifact_reviews_and_downloads_mobile_agenda_snapshot(
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click() page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle") page.wait_for_url(origin + "/", wait_until="networkidle")
page.evaluate(
"async () => { const lifecycle = await window.stackchainWorkspaceLifecycle; "
"await lifecycle.hydrateWorkspace(); await lifecycle.workspaceReady; }"
)
page.locator('[data-mobile-task="queues"]').click() page.locator('[data-mobile-task="queues"]').click()
page.locator('[data-mobile-queue="agenda"]').click() page.locator('[data-mobile-queue="agenda"]').click()

View File

@ -33,6 +33,15 @@ def test_page_runtime_is_one_deterministic_content_addressed_bundle(tmp_path):
assert first.runtime_name.startswith("runtime-") assert first.runtime_name.startswith("runtime-")
assert first.runtime_name.endswith(".js") assert first.runtime_name.endswith(".js")
assert len(gzip.compress(first.runtime_bytes, mtime=0)) <= 100 * 1024 assert len(gzip.compress(first.runtime_bytes, mtime=0)) <= 100 * 1024
initial_mobile_javascript = (
len(first.runtime_gzip_bytes)
+ len(first.feature_bundles["work-core"].runtime_gzip_bytes)
)
assert initial_mobile_javascript <= 60 * 1024
assert (
len(first.feature_bundles["today-timer"].runtime_gzip_bytes)
+ len(first.feature_bundles["planning"].runtime_gzip_bytes)
) > initial_mobile_javascript
changed_frontend = tmp_path / "frontend" changed_frontend = tmp_path / "frontend"
shutil.copytree(FRONTEND, changed_frontend) shutil.copytree(FRONTEND, changed_frontend)
@ -100,9 +109,13 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
) )
optional_block = optional_block.split("];", 1)[0] optional_block = optional_block.split("];", 1)[0]
assert f"BASE + '{first.feature_bundles['today-timer'].runtime_name}'" not in shell_block assert f"BASE + '{first.feature_bundles['today-timer'].runtime_name}'" not in shell_block
demand_loaded = {"today-timer", "planning"}
for name, bundle in first.feature_bundles.items(): for name, bundle in first.feature_bundles.items():
assert f"BASE + '{bundle.runtime_name}'" not in shell_block assert f"BASE + '{bundle.runtime_name}'" not in shell_block
assert f"BASE + '{bundle.runtime_name}'" in optional_block if name in demand_loaded:
assert f"BASE + '{bundle.runtime_name}'" not in optional_block
else:
assert f"BASE + '{bundle.runtime_name}'" in optional_block
changed_frontend = tmp_path / "frontend" changed_frontend = tmp_path / "frontend"
shutil.copytree(FRONTEND, changed_frontend) shutil.copytree(FRONTEND, changed_frontend)

View File

@ -19,7 +19,7 @@ const loadWorkspace = require({json.dumps(str(BOOTSTRAP))});
return json.loads(completed.stdout) return json.loads(completed.stdout)
def test_workspace_bootstrap_loads_content_addressed_feature_before_startup(): def test_workspace_bootstrap_demand_loads_optional_features_after_work_core():
result = run_bootstrap(""" result = run_bootstrap("""
const status={textContent:''}; const status={textContent:''};
const document={ const document={
@ -33,11 +33,14 @@ const document={
}; };
const requested=[]; const requested=[];
const createLoader=options=>({load:async name=>{requested.push(name + ':' + options.urls[name]);}}); const createLoader=options=>({load:async name=>{requested.push(name + ':' + options.urls[name]);}});
await loadWorkspace({document,createLoader}); const lifecycle=await loadWorkspace({document,createLoader});
console.log(JSON.stringify({requested,status:status.textContent})); const before=requested.slice();
await lifecycle.hydrateWorkspace?.();
console.log(JSON.stringify({before,after:requested,status:status.textContent}));
""") """)
assert result == { assert result == {
"requested": [ "before": ["work-core:feature-work-core-123.js"],
"after": [
"work-core:feature-work-core-123.js", "work-core:feature-work-core-123.js",
"today-timer:feature-workspace-abc.js", "today-timer:feature-workspace-abc.js",
"planning:feature-planning-def.js", "planning:feature-planning-def.js",
@ -46,7 +49,65 @@ console.log(JSON.stringify({requested,status:status.textContent}));
} }
def test_workspace_bootstrap_returns_after_work_core_while_optional_features_hydrate(): def test_workspace_bootstrap_hydrates_once_and_replays_dependent_mobile_action():
result = run_bootstrap("""
const listeners={}; const requested=[];
const document={
querySelector(selector) {
const match=selector.match(/stackchain-feature-([^\"]+)/);
return match ? {content:'feature-' + match[1] + '.js'} : null;
},
addEventListener(name,callback,capture){listeners[name]={callback,capture};},
removeEventListener(name,callback,capture){
if(listeners[name]?.callback===callback && listeners[name]?.capture===capture) delete listeners[name];
},
};
const createLoader=()=>({load:async name=>{
requested.push(name);
if(name==='today-timer') setImmediate(()=>{
actionReady=true;
lifecycle.markWorkspaceReady?.();
});
}});
let lifecycle=await loadWorkspace({document,createLoader});
let prevented=0,stopped=0,replayed=0,actionReady=false,replayedReady=false;
const target={
closest(selector){return selector.includes('data-mobile-task') ? this : null;},
click(){replayed++; replayedReady=actionReady;},
};
await listeners.click?.callback({target,preventDefault(){prevented++;},stopImmediatePropagation(){stopped++;}});
console.log(JSON.stringify({requested,prevented,stopped,replayed,replayedReady,listening:Boolean(listeners.click)}));
""")
assert result == {
"requested": ["work-core", "today-timer", "planning"],
"prevented": 1,
"stopped": 1,
"replayed": 1,
"replayedReady": True,
"listening": False,
}
def test_workspace_bootstrap_hydrates_immediately_for_deep_link():
result = run_bootstrap("""
const requested=[];
const document={
querySelector(selector) {
const match=selector.match(/stackchain-feature-([^\"]+)/);
return match ? {content:'feature-' + match[1] + '.js'} : null;
},
addEventListener(){},
};
const window={location:{hash:'#/my-work/today'},addEventListener(){},removeEventListener(){}};
const createLoader=()=>({load:async name=>{requested.push(name);}});
const lifecycle=await loadWorkspace({document,window,createLoader});
await lifecycle.deepLinkReady;
console.log(JSON.stringify({requested}));
""")
assert result == {"requested": ["work-core", "today-timer", "planning"]}
def test_workspace_bootstrap_returns_after_work_core_before_optional_features_hydrate():
result = run_bootstrap(""" result = run_bootstrap("""
const document={querySelector(selector) { const document={querySelector(selector) {
const match=selector.match(/stackchain-feature-([^\"]+)/); const match=selector.match(/stackchain-feature-([^\"]+)/);
@ -60,12 +121,13 @@ const createLoader=()=>({load:name=>{
}}); }});
const lifecycle=await loadWorkspace({document,createLoader}); const lifecycle=await loadWorkspace({document,createLoader});
const returned=requested.slice(); const returned=requested.slice();
const hydration=lifecycle.hydrateWorkspace();
releases['today-timer'](); releases.planning(); releases['today-timer'](); releases.planning();
await lifecycle.optionalReady; await hydration;
console.log(JSON.stringify({returned,settled:requested})); console.log(JSON.stringify({returned,settled:requested}));
""") """)
assert result == { assert result == {
"returned": ["work-core", "today-timer", "planning"], "returned": ["work-core"],
"settled": ["work-core", "today-timer", "planning"], "settled": ["work-core", "today-timer", "planning"],
} }
@ -81,7 +143,8 @@ const document={querySelector(selector) {
}}; }};
const createLoader=()=>({load:async()=>{attempts++; if (attempts === 1) throw new Error('brief outage');}}); const createLoader=()=>({load:async()=>{attempts++; if (attempts === 1) throw new Error('brief outage');}});
const schedule=callback=>{callback();}; const schedule=callback=>{callback();};
await loadWorkspace({document,createLoader,schedule}); const lifecycle=await loadWorkspace({document,createLoader,schedule});
await lifecycle.hydrateWorkspace();
console.log(JSON.stringify({attempts,status:status.textContent,retryHidden:retry.hidden})); console.log(JSON.stringify({attempts,status:status.textContent,retryHidden:retry.hidden}));
""") """)
assert result == {"attempts": 4, "status": "", "retryHidden": True} assert result == {"attempts": 4, "status": "", "retryHidden": True}
@ -107,7 +170,7 @@ await Promise.all([first,second,loading]);
console.log(JSON.stringify({attempts,reloads,offered,status:status.textContent,retryHidden:retry.hidden})); console.log(JSON.stringify({attempts,reloads,offered,status:status.textContent,retryHidden:retry.hidden}));
""") """)
assert result == { assert result == {
"attempts": 5, "attempts": 3,
"reloads": 0, "reloads": 0,
"offered": { "offered": {
"hidden": False, "hidden": False,
@ -151,7 +214,7 @@ await loading;
console.log(JSON.stringify({attempts,waiting,reloads,status:status.textContent})); console.log(JSON.stringify({attempts,waiting,reloads,status:status.textContent}));
""") """)
assert result == { assert result == {
"attempts": 5, "attempts": 3,
"waiting": True, "waiting": True,
"reloads": 0, "reloads": 0,
"status": "", "status": "",