@wenathlan/devthink 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/accounts.ts +2087 -0
- package/agent.ts +2331 -0
- package/antigravity.ts +2139 -0
- package/apifreeze.ts +4146 -0
- package/auth.ts +642 -0
- package/background.ts +16837 -0
- package/bridge.ts +2080 -0
- package/build.ts +54 -0
- package/bun.ts +26 -0
- package/capture.ts +1614 -0
- package/checksecrets.ts +72 -0
- package/cjs.ts +9 -0
- package/cli.ts +3500 -0
- package/commands.ts +1101 -0
- package/companion.ts +450 -0
- package/compatibility.ts +37 -0
- package/config.ts +3137 -0
- package/constants.ts +943 -0
- package/core.ts +1473 -0
- package/crossbrowser.ts +1359 -0
- package/dashdone.ts +605 -0
- package/data.ts +943 -0
- package/database.ts +133 -0
- package/debug.ts +850 -0
- package/deno.ts +27 -0
- package/devthink.ts +707 -0
- package/dist/accounts.d.ts +409 -0
- package/dist/agent.d.ts +837 -0
- package/dist/antigravity.d.ts +380 -0
- package/dist/apifreeze.d.ts +113 -0
- package/dist/auth.d.ts +224 -0
- package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
- package/dist/capture.d.ts +595 -0
- package/dist/cjs.d.ts +6 -0
- package/{cli.js → dist/cli.js} +2 -2
- package/dist/commands.d.ts +310 -0
- package/dist/compatibility.d.ts +17 -0
- package/dist/config.d.ts +1853 -0
- package/dist/constants.d.ts +637 -0
- package/dist/core.d.ts +361 -0
- package/dist/data.d.ts +330 -0
- package/dist/database.d.ts +24 -0
- package/dist/debug.d.ts +270 -0
- package/dist/engine.d.ts +74 -0
- package/dist/environments.d.ts +244 -0
- package/dist/evidence.d.ts +277 -0
- package/dist/export.d.ts +311 -0
- package/dist/fingerprint.d.ts +594 -0
- package/dist/flow.d.ts +300 -0
- package/dist/gates.d.ts +103 -0
- package/dist/identity.d.ts +51 -0
- package/dist/ids.d.ts +7 -0
- package/dist/llm.d.ts +652 -0
- package/dist/models.d.ts +1807 -0
- package/dist/net.d.ts +325 -0
- package/dist/neutral.d.ts +41 -0
- package/dist/oauth.d.ts +703 -0
- package/dist/page.d.ts +1166 -0
- package/dist/perf.d.ts +481 -0
- package/dist/plan.d.ts +134 -0
- package/dist/project.d.ts +395 -0
- package/dist/providers.d.ts +37 -0
- package/dist/quota.d.ts +453 -0
- package/dist/recovery.d.ts +621 -0
- package/dist/request.d.ts +1492 -0
- package/dist/run.d.ts +779 -0
- package/dist/runtime.d.ts +212 -0
- package/dist/search.d.ts +375 -0
- package/dist/security.d.ts +534 -0
- package/dist/serve.d.ts +136 -0
- package/dist/session.d.ts +296 -0
- package/dist/storage.d.ts +53 -0
- package/dist/streaming.d.ts +1119 -0
- package/dist/swarm.d.ts +328 -0
- package/dist/tools.d.ts +170 -0
- package/dist/types.d.ts +8357 -0
- package/dist/utils.d.ts +82 -0
- package/dist/version.d.ts +7 -0
- package/dist/versionregistry.d.ts +406 -0
- package/dist/views.d.ts +579 -0
- package/dist/workbenchsession.d.ts +69 -0
- package/dist/workflow.d.ts +649 -0
- package/docs/readme.md +92 -0
- package/docs/schemas/antigravity.json +192 -0
- package/engine.ts +1524 -0
- package/environments.ts +1005 -0
- package/evidence.ts +643 -0
- package/export.ts +889 -0
- package/fingerprint.ts +1458 -0
- package/flow.ts +1190 -0
- package/gates.ts +307 -0
- package/gateway.ts +34 -0
- package/hardening.ts +191 -0
- package/headless.ts +369 -0
- package/http.ts +2110 -0
- package/identity.ts +246 -0
- package/ids.ts +26 -0
- package/index.ts +369 -0
- package/llm.ts +2682 -0
- package/mcp.ts +1835 -0
- package/{memory.js → memory.ts} +3834 -2310
- package/models.ts +3805 -0
- package/modes.ts +43 -0
- package/net.ts +1214 -0
- package/neutral.ts +362 -0
- package/node.ts +26 -0
- package/oauth.ts +2216 -0
- package/offscreen.ts +170 -0
- package/pack.ts +956 -0
- package/package.json +175 -82
- package/page.ts +7824 -0
- package/pagebridge.ts +1116 -0
- package/perf.ts +957 -0
- package/plan.ts +672 -0
- package/plugin.ts +76 -0
- package/{policy.js → policy.ts} +5073 -4679
- package/prisma.config.ts +27 -0
- package/progress.ts +1308 -0
- package/project.ts +1291 -0
- package/protocol.ts +4187 -0
- package/providers.ts +246 -0
- package/quota.ts +1887 -0
- package/recovery.ts +2433 -0
- package/request.ts +4482 -0
- package/run.ts +2135 -0
- package/runtime.ts +526 -0
- package/search.ts +1760 -0
- package/security.ts +1673 -0
- package/serve.ts +419 -0
- package/server.ts +5538 -0
- package/session.ts +1162 -0
- package/storage.ts +182 -0
- package/streaming.ts +4101 -0
- package/swarm.ts +884 -0
- package/sync.ts +56 -0
- package/tests/agentcert.test.ts +128 -0
- package/tests/agentfleet.test.ts +214 -0
- package/tests/agentreview.test.ts +241 -0
- package/tests/agentstream.test.ts +335 -0
- package/tests/agentwork.test.ts +356 -0
- package/tests/apifreeze.test.ts +290 -0
- package/tests/apimap.test.ts +112 -0
- package/tests/approvalgate.test.ts +175 -0
- package/tests/artifactmanifest.test.ts +204 -0
- package/tests/attentionfeed.test.ts +163 -0
- package/tests/backgroundruns.test.ts +100 -0
- package/tests/batchscheduling.test.ts +131 -0
- package/tests/blackboard.test.ts +238 -0
- package/tests/bridge.test.ts +1088 -0
- package/tests/browsercoverage.test.ts +134 -0
- package/tests/browserpolyfills.test.ts +124 -0
- package/tests/build.mjs +1260 -0
- package/tests/clientauth.test.ts +223 -0
- package/tests/clitools.test.ts +578 -0
- package/tests/confirmgates.test.ts +190 -0
- package/tests/containerpack.test.ts +143 -0
- package/tests/controlflow.test.ts +2120 -0
- package/tests/coordination.test.ts +345 -0
- package/tests/copyscreen.test.ts +139 -0
- package/tests/core.test.ts +417 -0
- package/tests/costcert.test.ts +95 -0
- package/tests/crossbrowser.test.ts +115 -0
- package/tests/cspaudit.test.ts +79 -0
- package/tests/dashdone.test.ts +615 -0
- package/tests/datagrid.test.ts +137 -0
- package/tests/doccheck.test.ts +107 -0
- package/tests/dombatch.test.ts +109 -0
- package/tests/emulation.test.ts +282 -0
- package/tests/environments.test.ts +259 -0
- package/tests/evidenceviews.test.ts +88 -0
- package/tests/exporttools.test.ts +185 -0
- package/tests/firefoxprep.test.ts +134 -0
- package/tests/flowlibrary.test.ts +242 -0
- package/tests/flowrun.test.ts +303 -0
- package/tests/forensics.test.ts +439 -0
- package/tests/gateway.test.ts +939 -0
- package/tests/headlesslib.test.ts +171 -0
- package/tests/httpstream.test.ts +277 -0
- package/tests/immutablelog.test.ts +157 -0
- package/tests/inboundguard.test.ts +177 -0
- package/tests/lazyload.test.ts +72 -0
- package/tests/librarymodes.test.ts +623 -0
- package/tests/llm.test.ts +696 -0
- package/tests/manifest.test.ts +226 -0
- package/tests/maskinputs.test.ts +150 -0
- package/tests/matrixverify.test.ts +111 -0
- package/tests/mavenpack.test.ts +75 -0
- package/tests/mcpserver.test.ts +704 -0
- package/tests/memory.test.ts +4155 -0
- package/tests/memorycare.test.ts +230 -0
- package/tests/migrateplan.test.ts +349 -0
- package/tests/minimization.test.ts +414 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/multiagent.test.ts +440 -0
- package/tests/nativebridge.test.ts +807 -0
- package/tests/navigation.test.ts +284 -0
- package/tests/nugetpack.test.ts +83 -0
- package/tests/orchestration.test.ts +449 -0
- package/tests/originpolicy.test.ts +405 -0
- package/tests/outputcompare.test.ts +97 -0
- package/tests/package.test.ts +91 -0
- package/tests/pagebridge.test.ts +5909 -0
- package/tests/pentest.test.ts +75 -0
- package/tests/perfrecords.test.ts +257 -0
- package/tests/permdiff.test.ts +85 -0
- package/tests/phishguard.test.ts +128 -0
- package/tests/pickerviews.test.ts +155 -0
- package/tests/pipelines.test.ts +381 -0
- package/tests/planlint.test.ts +240 -0
- package/tests/planreview.test.ts +383 -0
- package/tests/platformtargets.test.ts +56 -0
- package/tests/policy.test.ts +8284 -0
- package/tests/portability.test.ts +96 -0
- package/tests/progress.test.ts +1036 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/protocol.test.ts +4793 -0
- package/tests/publishingpipeline.test.ts +314 -0
- package/tests/quickactions.test.ts +164 -0
- package/tests/readiness.test.ts +78 -0
- package/tests/recipes.test.ts +163 -0
- package/tests/redactshots.test.ts +216 -0
- package/tests/registernvidiakeys.ts +51 -0
- package/tests/relayserve.test.ts +304 -0
- package/tests/releasenotes.test.ts +276 -0
- package/tests/resilience.test.ts +467 -0
- package/tests/resourceaware.test.ts +187 -0
- package/tests/runbudget.test.ts +125 -0
- package/tests/runhistory.test.ts +299 -0
- package/tests/runreplay.test.ts +113 -0
- package/tests/runresume.test.ts +184 -0
- package/tests/runstate.test.ts +288 -0
- package/tests/runtimeadapters.test.ts +251 -0
- package/tests/safariskeleton.test.ts +115 -0
- package/tests/sandboxframe.test.ts +196 -0
- package/tests/sbom.test.ts +105 -0
- package/tests/secretvault.test.ts +188 -0
- package/tests/selcache.test.ts +111 -0
- package/tests/sessioninterface.test.ts +791 -0
- package/tests/sessions.test.ts +591 -0
- package/tests/siteprefs.test.ts +228 -0
- package/tests/snapshotdelta.test.ts +165 -0
- package/tests/statusviews.test.ts +237 -0
- package/tests/stepmeter.test.ts +200 -0
- package/tests/streamparse.test.ts +163 -0
- package/tests/stylepruning.test.ts +90 -0
- package/tests/surfaces.test.ts +282 -0
- package/tests/sweep.test.ts +78 -0
- package/tests/syncbridge.test.ts +164 -0
- package/tests/taskqueue.test.ts +206 -0
- package/tests/toolcalls.test.ts +336 -0
- package/tests/toolcatalog.test.ts +201 -0
- package/tests/torture-agent.test.ts +706 -0
- package/tests/torture-auth.test.ts +461 -0
- package/tests/torture-commands.test.ts +895 -0
- package/tests/torture-data.test.ts +923 -0
- package/tests/torture-environments.test.ts +285 -0
- package/tests/torture-export.test.ts +263 -0
- package/tests/torture-http.test.ts +1379 -0
- package/tests/torture-llm.test.ts +808 -0
- package/tests/torture-memory.test.ts +1084 -0
- package/tests/torture-plan.test.ts +829 -0
- package/tests/torture-run.test.ts +626 -0
- package/tests/torture-session.test.ts +928 -0
- package/tests/torture-swarm.test.ts +1245 -0
- package/tests/torture-tools.test.ts +964 -0
- package/tests/torture-workflow.test.ts +2354 -0
- package/tests/tourviews.test.ts +52 -0
- package/tests/transparency.test.ts +147 -0
- package/tests/trigger.test.ts +941 -0
- package/tests/v1sunset.test.ts +428 -0
- package/tests/virtlist.test.ts +94 -0
- package/tests/vision.test.ts +540 -0
- package/tests/vsixpack.test.ts +145 -0
- package/tests/webapi.test.ts +523 -0
- package/tests/workflow.test.ts +1785 -0
- package/tests/workfloweditor.test.ts +759 -0
- package/tests/xpipack.test.ts +128 -0
- package/tools.ts +734 -0
- package/types.ts +9908 -0
- package/umd.ts +9 -0
- package/utils.ts +249 -0
- package/version.ts +8 -0
- package/versionregistry.ts +1310 -0
- package/views.ts +2153 -0
- package/vitest.config.ts +12 -0
- package/web/App.tsx +78 -0
- package/web/README.md +47 -0
- package/web/capacitor.config.ts +46 -0
- package/web/const.ts +19 -0
- package/web/control.shell.tsx +66 -0
- package/web/dashboardpage.ts +554 -0
- package/web/db.ts +493 -0
- package/web/design.html +541 -0
- package/web/gatewayview/Gateway.tsx +345 -0
- package/web/gatewayview/config.ts +1042 -0
- package/web/gatewayview/definition.ts +174 -0
- package/web/icons.ts +15 -0
- package/{manifest.json → web/manifest.json} +1 -1
- package/web/optionspage.ts +691 -0
- package/web/popup.ts +1651 -0
- package/web/schema.prisma +176 -0
- package/web/sidepanel.ts +7941 -0
- package/web/sitemanifest.json +13 -0
- package/web/terminal-ui.tsx +622 -0
- package/web/transparencypage.ts +541 -0
- package/web/vite.config.ts +39 -0
- package/web/workspace.ts +23 -0
- package/workbenchmemory.ts +104 -0
- package/workbenchsession.ts +304 -0
- package/workflow.ts +4495 -0
- package/bridge.d.ts.map +0 -1
- package/bridge.js +0 -1877
- package/bun.d.ts.map +0 -1
- package/bun.js +0 -52329
- package/caps/background.json +0 -712
- package/caps/cli.json +0 -360
- package/caps/library.json +0 -3611
- package/caps/mcp.json +0 -126
- package/caps/pagebridge.json +0 -377
- package/caps/popup.json +0 -51
- package/caps/sidepanel.json +0 -522
- package/checksums.txt +0 -142
- package/cli.d.ts.map +0 -1
- package/companion.d.ts.map +0 -1
- package/companion.js +0 -389
- package/crossbrowser.d.ts.map +0 -1
- package/crossbrowser.js +0 -1191
- package/dashdone.d.ts.map +0 -1
- package/dashdone.js +0 -368
- package/deno.d.ts.map +0 -1
- package/deno.js +0 -52325
- package/devthink.umd.js +0 -52346
- package/fixtures/bun.ts +0 -7
- package/fixtures/cjs.js +0 -6
- package/fixtures/deno.ts +0 -6
- package/fixtures/example-org-pagestate.json +0 -22
- package/fixtures/importers/automa-workflow.json +0 -18
- package/fixtures/importers/selenium-side.json +0 -20
- package/fixtures/importers/tabular-plan.csv +0 -7
- package/fixtures/importers/uivision-macro.json +0 -10
- package/fixtures/importers/v1-plan.json +0 -13
- package/fixtures/mcp-client.mjs +0 -14
- package/fixtures/pages/checkout.html +0 -192
- package/fixtures/pages/dashboard.html +0 -110
- package/fixtures/pages/feed.html +0 -113
- package/fixtures/pages/product-grid.html +0 -150
- package/fixtures/plans/form-inventory-plan.json +0 -11
- package/fixtures/plans/release-notes-plan.json +0 -11
- package/fixtures/recipes/agentscompete.json +0 -11
- package/fixtures/recipes/agentsforms.json +0 -11
- package/fixtures/recipes/agentsmonitor.json +0 -11
- package/fixtures/recipes/agentsreview.json +0 -29
- package/fixtures/recipes/agentswarm.json +0 -11
- package/fixtures/recipes/fillcalendar.json +0 -12
- package/fixtures/recipes/fillcheckout.json +0 -13
- package/fixtures/recipes/filldependentdropdowns.json +0 -12
- package/fixtures/recipes/fillfromcsv.json +0 -13
- package/fixtures/recipes/fillloginform.json +0 -12
- package/fixtures/recipes/fillmultistepwizard.json +0 -13
- package/fixtures/recipes/fillregistration.json +0 -12
- package/fixtures/recipes/fillsearchfilters.json +0 -12
- package/fixtures/recipes/filltypeahead.json +0 -11
- package/fixtures/recipes/monitoralerts.json +0 -12
- package/fixtures/recipes/monitoravailability.json +0 -11
- package/fixtures/recipes/monitornetworkquiet.json +0 -12
- package/fixtures/recipes/monitorpagechanges.json +0 -11
- package/fixtures/recipes/monitorpricechange.json +0 -11
- package/fixtures/recipes/monitorschedule.json +0 -12
- package/fixtures/recipes/scrapedynamicprices.json +0 -13
- package/fixtures/recipes/scrapeimagegallery.json +0 -12
- package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
- package/fixtures/recipes/scrapenestedlists.json +0 -12
- package/fixtures/recipes/scrapeproductgrid.json +0 -12
- package/fixtures/recipes/scrapereviews.json +0 -12
- package/fixtures/recipes/scrapesearchresults.json +0 -12
- package/fixtures/recipes/scrapetablepagination.json +0 -12
- package/fixtures/recipes/testaccessibility.json +0 -34
- package/fixtures/recipes/testconsoleerrors.json +0 -12
- package/fixtures/recipes/testdeeplinks.json +0 -12
- package/fixtures/recipes/testformvalidation.json +0 -13
- package/fixtures/recipes/testlayoutshift.json +0 -12
- package/fixtures/recipes/testlinkcheck.json +0 -12
- package/fixtures/recipes/testloadtiming.json +0 -12
- package/fixtures/recipes/testvisualdiff.json +0 -12
- package/fixtures/workflows/changelog-digest-workflow.json +0 -13
- package/fixtures/workflows/form-survey-workflow.json +0 -13
- package/gallery.json +0 -706
- package/hardening.d.ts.map +0 -1
- package/hardening.js +0 -188
- package/headless.d.ts.map +0 -1
- package/headless.js +0 -11522
- package/http.d.ts.map +0 -1
- package/http.js +0 -2165
- package/index.cjs +0 -54682
- package/index.d.ts.map +0 -1
- package/index.js +0 -52310
- package/index.neutral.js +0 -46343
- package/mcp.d.ts.map +0 -1
- package/mcp.js +0 -20515
- package/memory.d.ts.map +0 -1
- package/nativehost.template.json +0 -10
- package/node.cjs +0 -54686
- package/node.d.ts.map +0 -1
- package/pack.d.ts.map +0 -1
- package/pack.js +0 -805
- package/policy.d.ts.map +0 -1
- package/progress.d.ts.map +0 -1
- package/progress.js +0 -645
- package/protocol.d.ts.map +0 -1
- package/protocol.js +0 -8334
- package/schemas/audit.schema.json +0 -226
- package/schemas/capability.schema.json +0 -113
- package/schemas/envelope.schema.json +0 -162
- package/schemas/memory.schema.json +0 -51
- package/schemas/observation.schema.json +0 -113
- package/schemas/plan.schema.json +0 -208
- package/schemas/progress.schema.json +0 -126
- package/schemas/proposal.schema.json +0 -68
- package/schemas/session.schema.json +0 -82
- package/schemas/tool.schema.json +0 -94
- package/server.d.ts.map +0 -1
- package/server.js +0 -28877
- package/umd-example.html +0 -18
- package/umd.d.ts.map +0 -1
- /package/{bun.d.ts → dist/bun.d.ts} +0 -0
- /package/{cli.d.ts → dist/cli.d.ts} +0 -0
- /package/{companion.d.ts → dist/companion.d.ts} +0 -0
- /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
- /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
- /package/{deno.d.ts → dist/deno.d.ts} +0 -0
- /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
- /package/{headless.d.ts → dist/headless.d.ts} +0 -0
- /package/{http.d.ts → dist/http.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
- /package/{memory.d.ts → dist/memory.d.ts} +0 -0
- /package/{node.d.ts → dist/node.d.ts} +0 -0
- /package/{pack.d.ts → dist/pack.d.ts} +0 -0
- /package/{policy.d.ts → dist/policy.d.ts} +0 -0
- /package/{progress.d.ts → dist/progress.d.ts} +0 -0
- /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
- /package/{server.d.ts → dist/server.d.ts} +0 -0
- /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/perf.ts
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The perf module of the 1.1.90 consolidation: every correlated variation of the hot path measurement and bounding logic interned in this one file, so the module family carries one surface without duplicate variations.
|
|
3
|
+
* The correlation is the hot path family: perfrecords holds the perf record stream every meter writes into (the worker backpressure, the queue depth samples, the snapshot request dedupe, the capture plan schedule and the pruning of the record window); snapshotdelta fingerprints the snapshot regions so unchanged ones skip recomputation; dombatch coalesces the dom bursts into debounced batches with their query plans; stepmeter measures the steps (the durationmeter that writes its samples into the perf records, the selectorprofile with its failure rates, the steptrace spans, the startupmeter of the cold start and the slowmo replay); resourceaware bounds the same hot paths against the device (the tab suspend plan, the artifact compression, the log pruning and the battery aware network backoff); batchscheduling paces the queues with the domain lanes, the polite delay, the adaptive poll and the request coalescing under backpressure; and lazyload keeps the heavy modules out of the first paint path inside the user startup budget.
|
|
4
|
+
* No window, budget, factor or threshold is ever hardcoded: every bound stays the user's choice with no engine cap, and the meters measure and never refuse.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* ── Merged from perfrecords.ts ── */
|
|
8
|
+
import type {
|
|
9
|
+
perfrecord,
|
|
10
|
+
queuedepthsample,
|
|
11
|
+
incrsnapshotdelta,
|
|
12
|
+
snapshotchange,
|
|
13
|
+
batchqueryplan,
|
|
14
|
+
debouncedbatch,
|
|
15
|
+
debounceprofile,
|
|
16
|
+
durationsample,
|
|
17
|
+
selectorstats,
|
|
18
|
+
slowmosession,
|
|
19
|
+
startupsample,
|
|
20
|
+
steptracespan,
|
|
21
|
+
artifactcompressrecord,
|
|
22
|
+
batteryawarestate,
|
|
23
|
+
logprunerule,
|
|
24
|
+
networkretryrule,
|
|
25
|
+
tabsuspendstate,
|
|
26
|
+
adaptivepollwindow,
|
|
27
|
+
backpressuresignal,
|
|
28
|
+
cadencecontrol,
|
|
29
|
+
coalescedrequest,
|
|
30
|
+
domainlane,
|
|
31
|
+
lazyloadrecord,
|
|
32
|
+
lazymoddescriptor,
|
|
33
|
+
startupbudget,
|
|
34
|
+
} from "./types.js";
|
|
35
|
+
import { randomid } from "./memory.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Perfrecords logic of the 1.1.68 family.
|
|
39
|
+
* Every step records its perf data beside its outcome: the duration, the query count, the cache hits and the delta flag with the provenance attached, the worker queue gains backpressure that defers parse tasks past the user configured depth instead of refusing them, the queue depth records over time for tuning, and the run summary charts the step durations.
|
|
40
|
+
* No budget ever refuses work: the deferral, the priority and the depth stay the user's choices, and the records stay advisory instruments of measurement.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** Records one perf record of an executed step: the duration, the query count and the cache hits the step cost beside the delta flag and the provenance every record attaches. */
|
|
44
|
+
export function perfrecordof(input: {
|
|
45
|
+
runid: string;
|
|
46
|
+
stepid: string;
|
|
47
|
+
duration: number;
|
|
48
|
+
queries: number;
|
|
49
|
+
cachehits: number;
|
|
50
|
+
delta: boolean;
|
|
51
|
+
now: number;
|
|
52
|
+
provenance?: { origin?: string; environment?: string; task?: string };
|
|
53
|
+
}): perfrecord {
|
|
54
|
+
if (input.runid.trim() === "") throw new Error("The perf record needs its run id.");
|
|
55
|
+
if (input.stepid.trim() === "") throw new Error("The perf record needs its step id.");
|
|
56
|
+
return {
|
|
57
|
+
id: randomid(),
|
|
58
|
+
runid: input.runid,
|
|
59
|
+
stepid: input.stepid,
|
|
60
|
+
duration: Math.max(input.duration, 0),
|
|
61
|
+
queries: Math.max(input.queries, 0),
|
|
62
|
+
cachehits: Math.max(input.cachehits, 0),
|
|
63
|
+
delta: input.delta,
|
|
64
|
+
at: input.now,
|
|
65
|
+
provenance: input.provenance ?? {},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Reads the backpressure verdict of one worker queue: parse tasks past the user configured depth defer and never refuse, and an absent depth keeps the queue unbounded. */
|
|
70
|
+
export function workerbackpressure(input: { depth: number; pending: number; configured?: number }): {
|
|
71
|
+
deferred: number;
|
|
72
|
+
admitted: number;
|
|
73
|
+
unbounded: boolean;
|
|
74
|
+
} {
|
|
75
|
+
if (input.configured === undefined) return { deferred: 0, admitted: input.pending, unbounded: true };
|
|
76
|
+
const admitted = Math.min(input.pending, input.configured);
|
|
77
|
+
return { deferred: Math.max(input.pending - input.configured, 0), admitted, unbounded: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Builds one worker queue depth sample for the tuning record: the pending parse count and the deferred count the backpressure held at one moment. */
|
|
81
|
+
export function queuedepthsampleof(input: { depth: number; deferred: number; now: number }): queuedepthsample {
|
|
82
|
+
return { at: input.now, depth: Math.max(input.depth, 0), deferred: Math.max(input.deferred, 0) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Reads the tuning view of the recorded queue depth samples: the peak depth, the deferred total and the span the samples cover. */
|
|
86
|
+
export function queuedepthview(samples: queuedepthsample[]): { peak: number; deferred: number; samples: number } {
|
|
87
|
+
return {
|
|
88
|
+
peak: samples.reduce((peak, sample) => Math.max(peak, sample.depth), 0),
|
|
89
|
+
deferred: samples.reduce((sum, sample) => sum + sample.deferred, 0),
|
|
90
|
+
samples: samples.length,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Builds the snapshot request key of one run so the executor deduplicates identical snapshot requests within the run: the same run, base ref and fingerprint answer from the same recomputation. */
|
|
95
|
+
export function snapshotrequestkey(input: { runid: string; stepid: string; fingerprint: string }): string {
|
|
96
|
+
return `${input.runid}:${input.stepid}:${input.fingerprint}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Deduplicates identical snapshot requests within one run: the first request computes and the repeats answer from the first result while the executor records the hit count. */
|
|
100
|
+
export function dedupesnapshotrequests(input: {
|
|
101
|
+
runid: string;
|
|
102
|
+
requests: Array<{ stepid: string; fingerprint: string }>;
|
|
103
|
+
}): { distinct: Array<{ stepid: string; fingerprint: string }>; duplicates: number } {
|
|
104
|
+
const keys = new Set<string>();
|
|
105
|
+
const distinct: Array<{ stepid: string; fingerprint: string }> = [];
|
|
106
|
+
let duplicates = 0;
|
|
107
|
+
for (const request of input.requests) {
|
|
108
|
+
const key = snapshotrequestkey({ runid: input.runid, stepid: request.stepid, fingerprint: request.fingerprint });
|
|
109
|
+
if (keys.has(key)) {
|
|
110
|
+
duplicates += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
keys.add(key);
|
|
114
|
+
distinct.push(request);
|
|
115
|
+
}
|
|
116
|
+
return { distinct, duplicates };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Reads the capture schedule behind the user priority choice: speed defers the heavy capture work to the run end while evidence keeps every capture beside its step; the choice optimizes the order and never drops a capture. */
|
|
120
|
+
export function captureplanschedule(input: { priority?: "speed" | "evidence"; heavy: string[] }): {
|
|
121
|
+
beside: string[];
|
|
122
|
+
deferred: string[];
|
|
123
|
+
note: string;
|
|
124
|
+
} {
|
|
125
|
+
if (input.priority === "speed")
|
|
126
|
+
return {
|
|
127
|
+
beside: [],
|
|
128
|
+
deferred: [...input.heavy],
|
|
129
|
+
note: `The user speed priority defers ${input.heavy.length} heavy capture task${input.heavy.length === 1 ? "" : "s"} to the run end; every capture still runs inside the reviewed plan.`,
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
beside: [...input.heavy],
|
|
133
|
+
deferred: [],
|
|
134
|
+
note: `The user evidence priority keeps ${input.heavy.length} heavy capture task${input.heavy.length === 1 ? "" : "s"} beside their steps.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Reads the abandoned parse task ids of one halted run so the executor cancels them: a task belongs to the halted run and never started inside the worker pool. */
|
|
139
|
+
export function abandonedparsetasks(input: {
|
|
140
|
+
halted: boolean;
|
|
141
|
+
runid: string;
|
|
142
|
+
pending: Array<{ id: string; runid: string; started: boolean }>;
|
|
143
|
+
}): { cancelled: string[]; kept: string[] } {
|
|
144
|
+
if (!input.halted) return { cancelled: [], kept: input.pending.map((task) => task.id) };
|
|
145
|
+
const cancelled = input.pending.filter((task) => task.runid === input.runid && !task.started).map((task) => task.id);
|
|
146
|
+
const kept = input.pending.filter((task) => !cancelled.includes(task.id)).map((task) => task.id);
|
|
147
|
+
return { cancelled, kept };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Builds the perf summary of one recent run the dashboardpage renders: the step count, the total and average duration, the query count and the cache hit ratio beside the delta share. */
|
|
151
|
+
export function perfsummary(records: perfrecord[]): {
|
|
152
|
+
steps: number;
|
|
153
|
+
duration: number;
|
|
154
|
+
average: number;
|
|
155
|
+
queries: number;
|
|
156
|
+
cachehits: number;
|
|
157
|
+
hitratio: number;
|
|
158
|
+
deltashare: number;
|
|
159
|
+
} {
|
|
160
|
+
if (records.length === 0)
|
|
161
|
+
return { steps: 0, duration: 0, average: 0, queries: 0, cachehits: 0, hitratio: 0, deltashare: 0 };
|
|
162
|
+
const duration = records.reduce((sum, record) => sum + record.duration, 0);
|
|
163
|
+
const queries = records.reduce((sum, record) => sum + record.queries, 0);
|
|
164
|
+
const cachehits = records.reduce((sum, record) => sum + record.cachehits, 0);
|
|
165
|
+
const deltas = records.filter((record) => record.delta).length;
|
|
166
|
+
return {
|
|
167
|
+
steps: records.length,
|
|
168
|
+
duration,
|
|
169
|
+
average: Math.round(duration / records.length),
|
|
170
|
+
queries,
|
|
171
|
+
cachehits,
|
|
172
|
+
hitratio: queries === 0 ? 0 : cachehits / queries,
|
|
173
|
+
deltashare: deltas / records.length,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Charts the step durations of one run over its steps: one point per step in execution order with the step id, the duration and the environment provenance. */
|
|
178
|
+
export function stepdurationchart(records: perfrecord[]): Array<{ stepid: string; duration: number; delta: boolean }> {
|
|
179
|
+
return [...records]
|
|
180
|
+
.sort((left, right) => left.at - right.at)
|
|
181
|
+
.map((record) => ({ stepid: record.stepid, duration: record.duration, delta: record.delta }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Builds the perf audit bundle of one run: every record with its provenance beside the summary, exported as one bundle the user reads. */
|
|
185
|
+
export function perfbundle(input: { runid: string; records: perfrecord[] }): {
|
|
186
|
+
runid: string;
|
|
187
|
+
summary: ReturnType<typeof perfsummary>;
|
|
188
|
+
records: perfrecord[];
|
|
189
|
+
} {
|
|
190
|
+
return { runid: input.runid, summary: perfsummary(input.records), records: [...input.records] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Prunes the perf records past the user configured retention window; an absent window keeps every record while the audit bundle always rebuilds from the survivors. */
|
|
194
|
+
export function pruneperfrecords(
|
|
195
|
+
records: perfrecord[],
|
|
196
|
+
retention: number | undefined,
|
|
197
|
+
now: number,
|
|
198
|
+
): { kept: perfrecord[]; pruned: number } {
|
|
199
|
+
if (retention === undefined) return { kept: records, pruned: 0 };
|
|
200
|
+
const kept = records.filter((record) => now - record.at < retention);
|
|
201
|
+
return { kept, pruned: records.length - kept.length };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ── Merged from snapshotdelta.ts ── */
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Snapshotdelta logic of the 1.1.68 family.
|
|
208
|
+
* The snapshot builder computes deltas against the previous snapshot of the same run: every region keeps a stable fingerprint, the delta emits only the changed regions with their fingerprints, and the full snapshot returns on the user configured cadence while an empty delta lets the executor skip the recomputation in full.
|
|
209
|
+
* The base snapshot ref must come from the same run: a delta against another run or against no base at all refuses, and the cadence stays the user's choice with no engine default.
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/** One region of a snapshot the delta engine compares: the region name and its content. */
|
|
213
|
+
export type snapshotregion = { region: string; content: string };
|
|
214
|
+
|
|
215
|
+
/** Stable local fingerprint of one region content so the delta engine recognizes unchanged regions without recomputation; the hash stays deterministic without a network dependency. */
|
|
216
|
+
export function regionfingerprint(region: string, content: string): string {
|
|
217
|
+
let hash = 0x811c9dc5;
|
|
218
|
+
const source = `${region}:${content}`;
|
|
219
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
220
|
+
hash ^= source.charCodeAt(index);
|
|
221
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
222
|
+
}
|
|
223
|
+
return hash.toString(16).padStart(8, "0");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Computes one incrsnapshot delta against the base snapshot of the same run: only the added, changed and removed regions enter the change set, and the full snapshot returns once the user configured cadence of deltas elapsed. */
|
|
227
|
+
export function snapshotdeltaof(input: {
|
|
228
|
+
baseref: string;
|
|
229
|
+
runid: string;
|
|
230
|
+
base: snapshotregion[];
|
|
231
|
+
current: snapshotregion[];
|
|
232
|
+
deltasince?: number;
|
|
233
|
+
cadence?: number;
|
|
234
|
+
now: number;
|
|
235
|
+
}): incrsnapshotdelta {
|
|
236
|
+
if (input.baseref.trim() === "")
|
|
237
|
+
throw new Error("The incrsnapshot delta needs its base snapshot ref; a baseless delta recomputes nothing.");
|
|
238
|
+
const basemap = new Map(
|
|
239
|
+
input.base.map((region) => [region.region, regionfingerprint(region.region, region.content)]),
|
|
240
|
+
);
|
|
241
|
+
const changes: snapshotchange[] = [];
|
|
242
|
+
for (const region of input.current) {
|
|
243
|
+
const fingerprint = regionfingerprint(region.region, region.content);
|
|
244
|
+
const basefingerprint = basemap.get(region.region);
|
|
245
|
+
if (basefingerprint === undefined) changes.push({ region: region.region, fingerprint, kind: "added" });
|
|
246
|
+
else if (basefingerprint !== fingerprint) changes.push({ region: region.region, fingerprint, kind: "changed" });
|
|
247
|
+
}
|
|
248
|
+
for (const [region, fingerprint] of basemap) {
|
|
249
|
+
if (!input.current.some((candidate) => candidate.region === region))
|
|
250
|
+
changes.push({ region, fingerprint, kind: "removed" });
|
|
251
|
+
}
|
|
252
|
+
const full = input.cadence !== undefined && (input.deltasince ?? 0) >= input.cadence;
|
|
253
|
+
const fingerprint =
|
|
254
|
+
changes.length === 0 ? "empty" : changes.map((change) => `${change.region}:${change.fingerprint}`).join("|");
|
|
255
|
+
return {
|
|
256
|
+
baseref: input.baseref,
|
|
257
|
+
runid: input.runid,
|
|
258
|
+
changes: full ? [] : changes,
|
|
259
|
+
fingerprint: full ? `full:${input.baseref}` : fingerprint,
|
|
260
|
+
full,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Reads whether one incrsnapshot delta is empty: an empty delta with no full snapshot lets the executor skip the whole recomputation because nothing changed. */
|
|
265
|
+
export function isemptydelta(delta: incrsnapshotdelta): boolean {
|
|
266
|
+
return !delta.full && delta.changes.length === 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Reads whether the executor skips the recomputation of one step: an empty delta against the same run base means the snapshot builder emits nothing and the step reuses the base observation. */
|
|
270
|
+
export function skipsrecomputation(delta: incrsnapshotdelta): boolean {
|
|
271
|
+
return isemptydelta(delta);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Reads the fingerprint of the change set one delta carries beside its base ref, so the executor deduplicates identical snapshot requests within a run. */
|
|
275
|
+
export function deltarequestkey(delta: incrsnapshotdelta): string {
|
|
276
|
+
return `${delta.runid}:${delta.baseref}:${delta.fingerprint}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Builds the base snapshot ref record of one run the memory keeps for its deltas: the ref, the run it belongs to and the time the run set it. */
|
|
280
|
+
export function snapshotbaserecord(input: { runid: string; ref: string; now: number }): {
|
|
281
|
+
id: string;
|
|
282
|
+
runid: string;
|
|
283
|
+
ref: string;
|
|
284
|
+
at: number;
|
|
285
|
+
} {
|
|
286
|
+
if (input.runid.trim() === "")
|
|
287
|
+
throw new Error("The snapshot base needs its run id; a base without its run never serves a delta.");
|
|
288
|
+
if (input.ref.trim() === "") throw new Error("The snapshot base needs its ref; a baseless delta never computes.");
|
|
289
|
+
return { id: randomid(), runid: input.runid, ref: input.ref, at: input.now };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Reads the regions one delta touched, in the incrsnapshot change format the progress records reuse beside the step outcomes. */
|
|
293
|
+
export function deltachangesof(delta: incrsnapshotdelta): snapshotchange[] {
|
|
294
|
+
return [...delta.changes];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ── Merged from dombatch.ts ── */
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Dombatch logic of the 1.1.68 family.
|
|
301
|
+
* The dom observer debounces rapid mutation bursts into one batch, debouncedom coalesces scroll, input and resize storms per window, and the snapshot builder batches every dom query into one pass: the batchquery plan folds repeated selectors so one snapshot costs one query pass, and the grouped selectors execute together inside one offscreen worker task.
|
|
302
|
+
* Every window stays the user's choice: an absent window passes events through uncoalesced, and no engine default ever caps a storm.
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/** Builds the debounce profiles of the user configured windows: only the event kinds with a window coalesce while the kinds without one pass every event through. */
|
|
306
|
+
export function debounceprofilesof(windows: Partial<Record<debounceprofile["kind"], number>>): debounceprofile[] {
|
|
307
|
+
const kinds: debounceprofile["kind"][] = ["scroll", "input", "resize", "mutation"];
|
|
308
|
+
return kinds.filter((kind) => windows[kind] !== undefined).map((kind) => ({ kind, window: windows[kind] as number }));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Coalesces one dom event storm into per kind batches: every event inside the window of its kind folds into the open batch, and a batch closes once the window since its first event elapsed. */
|
|
312
|
+
export function coalescedombursts(input: {
|
|
313
|
+
events: Array<{ kind: debounceprofile["kind"]; at: number }>;
|
|
314
|
+
profiles: debounceprofile[];
|
|
315
|
+
now: number;
|
|
316
|
+
}): debouncedbatch[] {
|
|
317
|
+
const batches: debouncedbatch[] = [];
|
|
318
|
+
for (const profile of input.profiles) {
|
|
319
|
+
const events = input.events
|
|
320
|
+
.filter((event) => event.kind === profile.kind)
|
|
321
|
+
.sort((left, right) => left.at - right.at);
|
|
322
|
+
if (events.length === 0) continue;
|
|
323
|
+
const first = events[0]?.at ?? 0;
|
|
324
|
+
let open: debouncedbatch = { kind: profile.kind, count: 0, firstat: first, lastat: first, closed: false };
|
|
325
|
+
for (const event of events) {
|
|
326
|
+
if (event.at - open.firstat >= profile.window) {
|
|
327
|
+
batches.push({ ...open, closed: true });
|
|
328
|
+
open = { kind: profile.kind, count: 1, firstat: event.at, lastat: event.at, closed: false };
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
open = { ...open, count: open.count + 1, lastat: event.at };
|
|
332
|
+
}
|
|
333
|
+
batches.push({ ...open, closed: input.now - open.firstat >= profile.window });
|
|
334
|
+
}
|
|
335
|
+
return batches;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Debounces one rapid mutation burst of the dom observer into a single batch: every mutation inside the user window folds into one batch the observer delivers once. */
|
|
339
|
+
export function mutationbatchof(input: {
|
|
340
|
+
mutations: Array<{ at: number; fingerprint?: string }>;
|
|
341
|
+
window?: number;
|
|
342
|
+
now: number;
|
|
343
|
+
}): debouncedbatch {
|
|
344
|
+
const sorted = [...input.mutations].sort((left, right) => left.at - right.at);
|
|
345
|
+
const firstat = sorted[0]?.at ?? input.now;
|
|
346
|
+
const lastat = sorted[sorted.length - 1]?.at ?? input.now;
|
|
347
|
+
const inside = sorted.filter((mutation) => input.window === undefined || mutation.at - firstat < input.window);
|
|
348
|
+
return {
|
|
349
|
+
kind: "mutation",
|
|
350
|
+
count: inside.length,
|
|
351
|
+
firstat,
|
|
352
|
+
lastat,
|
|
353
|
+
closed: input.window === undefined || input.now - firstat >= input.window,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Builds one batchquery plan from the selector list a snapshot needs: repeated selectors fold away so the plan carries every distinct selector exactly once and the snapshot costs one query pass. */
|
|
358
|
+
export function batchqueryplanof(selectors: string[]): batchqueryplan {
|
|
359
|
+
const distinct = [...new Set(selectors.map((selector) => selector.trim()).filter((selector) => selector !== ""))];
|
|
360
|
+
return { selectors: distinct, folded: selectors.length - distinct.length, onepass: true };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Executes one batchquery plan in a single pass: the resolver runs once per distinct selector and every repeated selector shares the resolution of its first occurrence. */
|
|
364
|
+
export function runbatchquery(input: { plan: batchqueryplan; resolver: (selector: string) => string }): {
|
|
365
|
+
resolutions: Record<string, string>;
|
|
366
|
+
queries: number;
|
|
367
|
+
} {
|
|
368
|
+
const resolutions: Record<string, string> = {};
|
|
369
|
+
for (const selector of input.plan.selectors) resolutions[selector] = input.resolver(selector);
|
|
370
|
+
return { resolutions, queries: input.plan.selectors.length };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Shapes one batchquery plan into the offscreen worker task payload the worker pool executes: the grouped selectors travel together so one worker task answers the whole pass. */
|
|
374
|
+
export function batchquerytaskof(input: { plan: batchqueryplan; runid: string; stepid: string }): {
|
|
375
|
+
task: "batchquery";
|
|
376
|
+
runid: string;
|
|
377
|
+
stepid: string;
|
|
378
|
+
selectors: string[];
|
|
379
|
+
folded: number;
|
|
380
|
+
} {
|
|
381
|
+
return {
|
|
382
|
+
task: "batchquery",
|
|
383
|
+
runid: input.runid,
|
|
384
|
+
stepid: input.stepid,
|
|
385
|
+
selectors: [...input.plan.selectors],
|
|
386
|
+
folded: input.plan.folded,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Reads the query savings of one batchquery pass: the folded duplicate count beside the distinct queries the pass ran. */
|
|
391
|
+
export function batchquerysavings(plan: batchqueryplan): { distinct: number; folded: number; saved: number } {
|
|
392
|
+
return { distinct: plan.selectors.length, folded: plan.folded, saved: plan.folded };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ── Merged from stepmeter.ts ── */
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Stepmeter logic of the 1.1.69 family.
|
|
399
|
+
* The durationmeter measures every step with monotonic clocks and writes its samples into the perf records of the 1.1.68 family, the selectorprofile counts the resolution time and the failure rate per selector and flags the selectors above the user latency threshold, the steptrace spans nest per step and per worker task and export one trace file for the timeline view, the startupmeter measures the cold start from the startup event to ready with the lazymods budget it spent, the coldstart keeps the ready path under the user target with the heavy modules out of the first paint path, and the slowmo replays a recorded run at the user chosen speed factor with its pauses linked to their steptrace spans.
|
|
400
|
+
* Every threshold, target and factor stays a user choice: the meters measure and never refuse.
|
|
401
|
+
*/
|
|
402
|
+
|
|
403
|
+
/** Builds one durationmeter sample from a monotonic clock pair: the step start and end clocks with the duration it measured. */
|
|
404
|
+
export function durationsampleof(input: {
|
|
405
|
+
stepid: string;
|
|
406
|
+
start: number;
|
|
407
|
+
end: number;
|
|
408
|
+
monotonic?: boolean;
|
|
409
|
+
}): durationsample {
|
|
410
|
+
if (input.stepid.trim() === "") throw new Error("The durationmeter sample needs its step id.");
|
|
411
|
+
return {
|
|
412
|
+
stepid: input.stepid,
|
|
413
|
+
start: input.start,
|
|
414
|
+
end: input.end,
|
|
415
|
+
duration: Math.max(input.end - input.start, 0),
|
|
416
|
+
monotonic: input.monotonic ?? true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Writes one durationmeter sample into the perf records of the 1.1.68 family: the sample becomes one perf record with its provenance attached. */
|
|
421
|
+
export function durationsampletoperf(input: {
|
|
422
|
+
runid: string;
|
|
423
|
+
sample: durationsample;
|
|
424
|
+
queries: number;
|
|
425
|
+
cachehits: number;
|
|
426
|
+
delta: boolean;
|
|
427
|
+
now: number;
|
|
428
|
+
provenance?: { origin?: string; environment?: string; task?: string };
|
|
429
|
+
}): perfrecord {
|
|
430
|
+
return perfrecordof({
|
|
431
|
+
runid: input.runid,
|
|
432
|
+
stepid: input.sample.stepid,
|
|
433
|
+
duration: input.sample.duration,
|
|
434
|
+
queries: input.queries,
|
|
435
|
+
cachehits: input.cachehits,
|
|
436
|
+
delta: input.delta,
|
|
437
|
+
now: input.now,
|
|
438
|
+
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Reads the selectorprofile stats of one selector: the resolution count, the total and average resolution time and the failure rate, with the flag the selectors above the user latency threshold carry. */
|
|
443
|
+
export function selectorprofileof(input: {
|
|
444
|
+
selector: string;
|
|
445
|
+
samples: Array<{ duration: number; ok: boolean }>;
|
|
446
|
+
threshold?: number;
|
|
447
|
+
}): selectorstats {
|
|
448
|
+
if (input.selector.trim() === "") throw new Error("The selectorprofile needs its selector.");
|
|
449
|
+
const count = input.samples.length;
|
|
450
|
+
if (count === 0)
|
|
451
|
+
return {
|
|
452
|
+
selector: input.selector,
|
|
453
|
+
count: 0,
|
|
454
|
+
totalduration: 0,
|
|
455
|
+
average: 0,
|
|
456
|
+
failures: 0,
|
|
457
|
+
failurerate: 0,
|
|
458
|
+
flagged: false,
|
|
459
|
+
};
|
|
460
|
+
const totalduration = input.samples.reduce((sum, sample) => sum + Math.max(sample.duration, 0), 0);
|
|
461
|
+
const failures = input.samples.filter((sample) => !sample.ok).length;
|
|
462
|
+
const average = Math.round(totalduration / count);
|
|
463
|
+
return {
|
|
464
|
+
selector: input.selector,
|
|
465
|
+
count,
|
|
466
|
+
totalduration,
|
|
467
|
+
average,
|
|
468
|
+
failures,
|
|
469
|
+
failurerate: failures / count,
|
|
470
|
+
flagged: input.threshold !== undefined && input.threshold > 0 && average > input.threshold,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Updates one selectorprofile stat with a fresh resolution sample beside its prior stats: the count, the total duration and the failure rate grow while the flag follows the user latency threshold. */
|
|
475
|
+
export function selectorstatsupdate(
|
|
476
|
+
prior: selectorstats | undefined,
|
|
477
|
+
input: { selector: string; duration: number; ok: boolean; threshold?: number },
|
|
478
|
+
): selectorstats {
|
|
479
|
+
const count = (prior?.count ?? 0) + 1;
|
|
480
|
+
const totalduration = (prior?.totalduration ?? 0) + Math.max(input.duration, 0);
|
|
481
|
+
const failures = (prior?.failures ?? 0) + (input.ok ? 0 : 1);
|
|
482
|
+
const average = Math.round(totalduration / count);
|
|
483
|
+
return {
|
|
484
|
+
selector: input.selector,
|
|
485
|
+
count,
|
|
486
|
+
totalduration,
|
|
487
|
+
average,
|
|
488
|
+
failures,
|
|
489
|
+
failurerate: failures / count,
|
|
490
|
+
flagged: input.threshold !== undefined && input.threshold > 0 && average > input.threshold,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Builds one steptrace span: the step or the worker task it covers with its start and end clocks, its cause and its optional parent span. */
|
|
495
|
+
export function steptracespanof(input: {
|
|
496
|
+
runid: string;
|
|
497
|
+
stepid?: string;
|
|
498
|
+
task?: string;
|
|
499
|
+
start: number;
|
|
500
|
+
end: number;
|
|
501
|
+
cause: string;
|
|
502
|
+
parent?: string;
|
|
503
|
+
}): steptracespan {
|
|
504
|
+
if (input.runid.trim() === "") throw new Error("The steptrace span needs its run id.");
|
|
505
|
+
return {
|
|
506
|
+
id: randomid(),
|
|
507
|
+
runid: input.runid,
|
|
508
|
+
...(input.stepid !== undefined ? { stepid: input.stepid } : {}),
|
|
509
|
+
...(input.task !== undefined ? { task: input.task } : {}),
|
|
510
|
+
start: input.start,
|
|
511
|
+
end: input.end,
|
|
512
|
+
cause: input.cause,
|
|
513
|
+
...(input.parent !== undefined ? { parent: input.parent } : {}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Reads the child spans of one steptrace span so the trace nests per step and per worker task. */
|
|
518
|
+
export function spanchildren(spans: steptracespan[], parentid: string): steptracespan[] {
|
|
519
|
+
return spans.filter((span) => span.parent === parentid);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Exports one trace file of a run for the timeline view: every span in start order with its event count. */
|
|
523
|
+
export function steptracefile(input: { runid: string; spans: steptracespan[] }): {
|
|
524
|
+
runid: string;
|
|
525
|
+
format: "devthink-steptrace";
|
|
526
|
+
events: number;
|
|
527
|
+
spans: steptracespan[];
|
|
528
|
+
} {
|
|
529
|
+
if (input.runid.trim() === "") throw new Error("The trace file needs its run id.");
|
|
530
|
+
const spans = [...input.spans].sort((left, right) => left.start - right.start);
|
|
531
|
+
return { runid: input.runid, format: "devthink-steptrace" as const, events: spans.length, spans };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Builds one startupmeter sample: the cold start duration from the startup event to ready with the lazymods budget it spent, reported against the user target without ever refusing a load. */
|
|
535
|
+
export function startupsampleof(input: {
|
|
536
|
+
startedat: number;
|
|
537
|
+
readyat: number;
|
|
538
|
+
spent: number;
|
|
539
|
+
target?: number;
|
|
540
|
+
}): startupsample {
|
|
541
|
+
return {
|
|
542
|
+
startedat: input.startedat,
|
|
543
|
+
readyat: input.readyat,
|
|
544
|
+
duration: Math.max(input.readyat - input.startedat, 0),
|
|
545
|
+
spent: Math.max(input.spent, 0),
|
|
546
|
+
...(input.target !== undefined ? { target: input.target } : {}),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Reads the coldstart verdict of one startup sample: the ready path stays under the user target while the heavy modules stay out of the first paint path; an absent target keeps the verdict informational only. */
|
|
551
|
+
export function coldstartverdict(input: { sample: startupsample; heavy: string[]; firstpaint: string[] }): {
|
|
552
|
+
withintarget: boolean;
|
|
553
|
+
heavyinfirstpaint: string[];
|
|
554
|
+
reason: string;
|
|
555
|
+
} {
|
|
556
|
+
const heavyinfirstpaint = input.firstpaint.filter((module) => input.heavy.includes(module));
|
|
557
|
+
const withintarget = input.sample.target === undefined ? true : input.sample.duration <= input.sample.target;
|
|
558
|
+
return {
|
|
559
|
+
withintarget,
|
|
560
|
+
heavyinfirstpaint,
|
|
561
|
+
reason: `${input.sample.target === undefined ? "The user set no cold start target and the engine sets none" : `The ready path of ${input.sample.duration} milliseconds stays ${withintarget ? "inside" : "past"} the user target of ${input.sample.target} milliseconds`}; ${heavyinfirstpaint.length} heavy module${heavyinfirstpaint.length === 1 ? "" : "s"} sit${heavyinfirstpaint.length === 1 ? "s" : ""} in the first paint path.`,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Opens one slowmo replay session of a recorded run at the user chosen speed factor; an absent factor keeps the replay at its recorded speed. */
|
|
566
|
+
export function slomosessionof(input: { runid: string; factor?: number; now: number }): slowmosession {
|
|
567
|
+
if (input.runid.trim() === "") throw new Error("The slowmo session needs its run id.");
|
|
568
|
+
const factor = input.factor !== undefined && input.factor > 0 ? input.factor : 1;
|
|
569
|
+
return { runid: input.runid, factor, paused: false, at: input.now };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Reads the pause window between two slowmo steps: the recorded duration divided by the user factor, so a factor of one half doubles every pause. */
|
|
573
|
+
export function slomostepdelay(input: { factor: number; duration: number }): number {
|
|
574
|
+
const factor = input.factor > 0 ? input.factor : 1;
|
|
575
|
+
return Math.max(input.duration / factor, 0);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Pauses one slowmo replay between steps for inspection and links the pause to its steptrace span. */
|
|
579
|
+
export function slomopause(input: { session: slowmosession; stepid: string; spanid: string }): slowmosession {
|
|
580
|
+
return { ...input.session, paused: true, stepid: input.stepid, spanid: input.spanid };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Resumes one slowmo replay from its inspection pause; the replay keeps its factor and its run. */
|
|
584
|
+
export function slomoresume(session: slowmosession): slowmosession {
|
|
585
|
+
return { ...session, paused: false };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* ── Merged from resourceaware.ts ── */
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Resourceaware logic of the 1.1.69 family.
|
|
592
|
+
* The tabsuspend discards idle tabs only during the waits longer than the user window, restores a tab before the next step that needs it and preserves the run state across the suspend and restore, the artifactcompress compresses captures and logs at rest and decompresses them lazily on read, the logprune removes the sealed logs past the user retention window while it prunes whole sealed runs only so the chain stays verifiable, the batteryaware scheduler defers non urgent scheduled runs on low battery and surfaces the deferrals in the attentionfeed, and the networkaware retry policies adapt their backoff to the failure kind while they honor the server signals when present.
|
|
593
|
+
* Every window, rule and floor stays a user choice: the deferrals, the codecs and the backoffs optimize the resource use and never refuse the work.
|
|
594
|
+
*/
|
|
595
|
+
|
|
596
|
+
/** Reads the tabsuspend plan of one wait: the tab suspends only during a wait longer than the user window while an absent window or a shorter wait never suspends the tab. */
|
|
597
|
+
export function suspendplan(input: { waitduration: number; window?: number }): { suspend: boolean; reason: string } {
|
|
598
|
+
if (input.window === undefined)
|
|
599
|
+
return {
|
|
600
|
+
suspend: false,
|
|
601
|
+
reason: "The user set no suspend window and the engine sets none; the tab never suspends during a wait.",
|
|
602
|
+
};
|
|
603
|
+
if (input.waitduration <= input.window)
|
|
604
|
+
return {
|
|
605
|
+
suspend: false,
|
|
606
|
+
reason: `The wait of ${input.waitduration} milliseconds stays inside the user suspend window of ${input.window} milliseconds; the tab never suspends.`,
|
|
607
|
+
};
|
|
608
|
+
return {
|
|
609
|
+
suspend: true,
|
|
610
|
+
reason: `The wait of ${input.waitduration} milliseconds runs past the user suspend window of ${input.window} milliseconds; the idle tab suspends while the run state stays preserved.`,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Builds one tabsuspend state: the suspended tab with its run, its restore url and the discard flag, so the run state survives the suspend and the restore. */
|
|
615
|
+
export function tabsuspendstateof(input: {
|
|
616
|
+
tabid: number;
|
|
617
|
+
runid: string;
|
|
618
|
+
restoreurl: string;
|
|
619
|
+
discarded: boolean;
|
|
620
|
+
now: number;
|
|
621
|
+
}): tabsuspendstate {
|
|
622
|
+
if (input.runid.trim() === "") throw new Error("The tabsuspend state needs its run id.");
|
|
623
|
+
return {
|
|
624
|
+
tabid: input.tabid,
|
|
625
|
+
runid: input.runid,
|
|
626
|
+
suspendedat: input.now,
|
|
627
|
+
restoreurl: input.restoreurl,
|
|
628
|
+
discarded: input.discarded,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Reads the restore plan of one suspended tab: the tab restores before the next step that needs it while a discarded tab reloads its restore url and an undiscarded tab keeps its page state. */
|
|
633
|
+
export function tabsuspendrestoreplan(input: { state: tabsuspendstate; nextneedsurl: string }): {
|
|
634
|
+
restore: boolean;
|
|
635
|
+
reload: boolean;
|
|
636
|
+
reason: string;
|
|
637
|
+
} {
|
|
638
|
+
if (!input.nextneedsurl.startsWith("http"))
|
|
639
|
+
return {
|
|
640
|
+
restore: true,
|
|
641
|
+
reload: input.state.discarded,
|
|
642
|
+
reason: `The next step needs the suspended tab ${input.state.tabid}${input.state.discarded ? "; the discarded tab reloads its restore url" : "; the tab keeps its page state"}.`,
|
|
643
|
+
};
|
|
644
|
+
if (input.nextneedsurl === input.state.restoreurl)
|
|
645
|
+
return {
|
|
646
|
+
restore: true,
|
|
647
|
+
reload: input.state.discarded,
|
|
648
|
+
reason: `The next step needs ${input.nextneedsurl} which the suspended tab ${input.state.tabid} restores${input.state.discarded ? " through a reload" : " with its page state intact"}.`,
|
|
649
|
+
};
|
|
650
|
+
return {
|
|
651
|
+
restore: false,
|
|
652
|
+
reload: false,
|
|
653
|
+
reason: `The next step needs ${input.nextneedsurl} while the suspended tab ${input.state.tabid} held ${input.state.restoreurl}; the navigation runs as reviewed.`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Builds one artifactcompress record: the stored artifact with its codec and its lazy read flag, so the capture and log bytes compress at rest and decompress on read. */
|
|
658
|
+
export function artifactcompressof(input: {
|
|
659
|
+
artifactid: string;
|
|
660
|
+
codec?: "deflate" | "store";
|
|
661
|
+
lazy?: boolean;
|
|
662
|
+
now: number;
|
|
663
|
+
}): artifactcompressrecord {
|
|
664
|
+
if (input.artifactid.trim() === "") throw new Error("The artifactcompress record needs its artifact id.");
|
|
665
|
+
return {
|
|
666
|
+
artifactid: input.artifactid,
|
|
667
|
+
codec: input.codec ?? "store",
|
|
668
|
+
compressedat: input.now,
|
|
669
|
+
lazy: input.lazy ?? true,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Reads the read plan of one stored artifact: a deflate artifact decompresses lazily on read while a stored artifact reads its plain bytes directly. */
|
|
674
|
+
export function artifactreadplan(record: artifactcompressrecord): { decode: "immediate" | "lazy"; reason: string } {
|
|
675
|
+
if (record.codec === "store")
|
|
676
|
+
return {
|
|
677
|
+
decode: "immediate",
|
|
678
|
+
reason: `The artifact ${record.artifactid} stores its bytes plain; the read needs no decompression.`,
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
decode: record.lazy ? "lazy" : "immediate",
|
|
682
|
+
reason: `The artifact ${record.artifactid} compresses at rest through ${record.codec} and decompresses ${record.lazy ? "lazily on read" : "at restore time"}.`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Reads the logprune plan of one sealed log chain: the prune removes whole sealed runs past the user retention or size window while an unsealed run never prunes so the chain stays verifiable. */
|
|
687
|
+
export function logpruneplan(input: {
|
|
688
|
+
sealed: Array<{ runid: string; sealedat: number; bytes: number; sealed: boolean }>;
|
|
689
|
+
rule: logprunerule;
|
|
690
|
+
now: number;
|
|
691
|
+
}): { prune: string[]; keep: string[]; refused: string[] } {
|
|
692
|
+
const prune: string[] = [];
|
|
693
|
+
const keep: string[] = [];
|
|
694
|
+
const refused: string[] = [];
|
|
695
|
+
for (const run of input.sealed) {
|
|
696
|
+
if (!run.sealed) {
|
|
697
|
+
refused.push(run.runid);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const aged = input.rule.retention !== undefined && input.now - run.sealedat >= input.rule.retention;
|
|
701
|
+
const oversized = input.rule.sizewindow !== undefined && run.bytes > input.rule.sizewindow;
|
|
702
|
+
if (aged || oversized) {
|
|
703
|
+
prune.push(run.runid);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
keep.push(run.runid);
|
|
707
|
+
}
|
|
708
|
+
return { prune, keep, refused };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Reads the batteryaware scheduling state: the non urgent scheduled runs defer while the battery stays under the user floor and never charges; the deferral never cancels a run. */
|
|
712
|
+
export function batteryawarestateof(input: {
|
|
713
|
+
level: number;
|
|
714
|
+
charging: boolean;
|
|
715
|
+
floor?: number;
|
|
716
|
+
scheduled: string[];
|
|
717
|
+
now: number;
|
|
718
|
+
}): batteryawarestate {
|
|
719
|
+
const low = input.floor !== undefined && input.floor > 0 && input.level < input.floor && !input.charging;
|
|
720
|
+
return {
|
|
721
|
+
level: Math.min(Math.max(input.level, 0), 1),
|
|
722
|
+
charging: input.charging,
|
|
723
|
+
deferred: low ? [...input.scheduled] : [],
|
|
724
|
+
at: input.now,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Reads the networkaware backoff of one retry: the policy of the failure kind sets the backoff window while a present server signal overrides it. */
|
|
729
|
+
export function networkbackoff(input: {
|
|
730
|
+
failurekind: string;
|
|
731
|
+
rules: networkretryrule[];
|
|
732
|
+
serversignal?: number;
|
|
733
|
+
}): number {
|
|
734
|
+
const rule = input.rules.find((candidate) => candidate.failurekind === input.failurekind);
|
|
735
|
+
if (input.serversignal !== undefined && input.serversignal > 0) return input.serversignal;
|
|
736
|
+
return rule !== undefined ? Math.max(rule.backoff, 0) : 0;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* ── Merged from batchscheduling.ts ── */
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Batchscheduling logic of the 1.1.69 family.
|
|
743
|
+
* Batch run queues pace themselves against the sites they address: the queue pauses its enqueueing when the downstream steps fall behind past the user window and never drops a queued step, every domain runs inside its user chosen concurrency slots with the overflow queued per lane, batch requests space themselves through the politedelay profile with its jitter and its per domain floors, the adaptivepoll window widens while observations stay unchanged and narrows the moment changes resume, identical pending queries merge into one dispatch whose single result fans out to every waiter, and the snapshot cadence widens under memory pressure and restores when the pressure clears.
|
|
744
|
+
* Every window, limit and factor stays a user choice: the scheduler optimizes the pacing and never caps the work, and no budget ever bypasses the review.
|
|
745
|
+
*/
|
|
746
|
+
|
|
747
|
+
/** Reads the backpressure signal of one batch run queue: the queue pauses its enqueueing when the completed outcomes fall behind the enqueued steps past the user window, and the pause never drops a queued step. */
|
|
748
|
+
export function batchbackpressuresignal(input: {
|
|
749
|
+
runid: string;
|
|
750
|
+
enqueued: number;
|
|
751
|
+
completed: number;
|
|
752
|
+
window?: number;
|
|
753
|
+
now: number;
|
|
754
|
+
}): backpressuresignal {
|
|
755
|
+
if (input.runid.trim() === "") throw new Error("The backpressure signal needs its run id.");
|
|
756
|
+
const behind = Math.max(input.enqueued - input.completed, 0);
|
|
757
|
+
const paused = input.window !== undefined && behind > input.window;
|
|
758
|
+
return {
|
|
759
|
+
runid: input.runid,
|
|
760
|
+
enqueued: input.enqueued,
|
|
761
|
+
completed: input.completed,
|
|
762
|
+
behind,
|
|
763
|
+
paused,
|
|
764
|
+
...(input.window !== undefined ? { window: input.window } : {}),
|
|
765
|
+
at: input.now,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Plans the domain lanes of one batch run: every domain runs at most its user chosen slots concurrently while the overflow steps queue per lane; a domain without a user limit stays unbounded. */
|
|
770
|
+
export function domainlanesfor(input: {
|
|
771
|
+
steps: Array<{ id: string; domain: string }>;
|
|
772
|
+
limits: Record<string, number>;
|
|
773
|
+
}): domainlane[] {
|
|
774
|
+
const domains = [...new Set(input.steps.map((step) => step.domain))];
|
|
775
|
+
return domains.map((domain) => {
|
|
776
|
+
const ids = input.steps.filter((step) => step.domain === domain).map((step) => step.id);
|
|
777
|
+
const limit = input.limits[domain];
|
|
778
|
+
if (limit === undefined) return { domain, slots: ids.length, running: [...ids], queued: [] };
|
|
779
|
+
const slots = Math.max(Math.floor(limit), 0);
|
|
780
|
+
return { domain, slots, running: ids.slice(0, slots), queued: ids.slice(slots) };
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Moves the queued overflow of one domain lane into its running set as its slots free; the lane never runs more steps than the user chosen slots. */
|
|
785
|
+
export function advancelane(lane: domainlane, freed: number): domainlane {
|
|
786
|
+
const room = Math.max(lane.slots - lane.running.length, 0);
|
|
787
|
+
const admit = lane.queued.slice(0, Math.min(Math.max(freed, 0), room));
|
|
788
|
+
return { ...lane, running: [...lane.running, ...admit], queued: lane.queued.slice(admit.length) };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Reads the politedelay of one domain: the base delay with the jitter window folded over it through the roll, never below the per domain floor the siteprofiles carry. */
|
|
792
|
+
export function politedelayfor(input: {
|
|
793
|
+
domain: string;
|
|
794
|
+
base: number;
|
|
795
|
+
floor?: number;
|
|
796
|
+
jitter?: number;
|
|
797
|
+
roll: number;
|
|
798
|
+
}): number {
|
|
799
|
+
if (input.domain.trim() === "") throw new Error("The politedelay needs its domain.");
|
|
800
|
+
const jitter = input.jitter ?? 0;
|
|
801
|
+
const delay = Math.max(input.base, 0) + Math.min(Math.max(input.roll, 0), 1) * Math.max(jitter, 0);
|
|
802
|
+
return Math.max(delay, input.floor ?? 0);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Reads the next adaptivepoll interval: the window widens by the growth factor while observations stay unchanged and narrows the moment changes resume, always inside the floor and the ceiling the user chose. */
|
|
806
|
+
export function adaptivepollnext(input: { interval: number; window: adaptivepollwindow; changed: boolean }): number {
|
|
807
|
+
if (input.window.growth <= 1) return Math.min(Math.max(input.interval, input.window.floor), input.window.ceiling);
|
|
808
|
+
const next = input.changed ? input.interval / input.window.growth : input.interval * input.window.growth;
|
|
809
|
+
return Math.min(Math.max(next, input.window.floor), input.window.ceiling);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Merges identical pending queries into one dispatch: every request with the same key folds into one coalesced entry whose waiters list carries every waiter the single result fans out to. */
|
|
813
|
+
export function coalescequeries(input: { requests: Array<{ key: string; waiter: string }> }): coalescedrequest[] {
|
|
814
|
+
const merged = new Map<string, coalescedrequest>();
|
|
815
|
+
for (const request of input.requests) {
|
|
816
|
+
const entry = merged.get(request.key);
|
|
817
|
+
if (entry === undefined) {
|
|
818
|
+
merged.set(request.key, { key: request.key, waiters: [request.waiter], dispatched: true });
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (!entry.waiters.includes(request.waiter)) entry.waiters.push(request.waiter);
|
|
822
|
+
}
|
|
823
|
+
return [...merged.values()];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** Fans the single result of one coalesced dispatch out to every waiter that merged into it. */
|
|
827
|
+
export function coalescefanout<T>(entry: coalescedrequest, result: T): Array<{ waiter: string; result: T }> {
|
|
828
|
+
return entry.waiters.map((waiter) => ({ waiter, result }));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Reads the snapshot cadence under memory pressure: the interval widens by the user chosen widening factor while the pressure stays high and restores to the base interval the moment the pressure clears. */
|
|
832
|
+
export function cadenceunderpressure(input: {
|
|
833
|
+
control: cadencecontrol;
|
|
834
|
+
pressure: boolean;
|
|
835
|
+
widening?: number;
|
|
836
|
+
}): cadencecontrol {
|
|
837
|
+
if (!input.pressure) return { base: input.control.base, current: input.control.base, pressure: false };
|
|
838
|
+
const widening = input.widening !== undefined && input.widening > 1 ? input.widening : 1;
|
|
839
|
+
const widened = input.control.pressure ? input.control.current : input.control.base * widening;
|
|
840
|
+
return { base: input.control.base, current: Math.max(widened, input.control.base), pressure: true };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* ── Merged from lazyload.ts ── */
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Lazyload logic of the 1.1.68 family.
|
|
847
|
+
* The heavy modules stay out of the startup path: every lazy module declares its id, its load reason and its capability requirements ahead of load, the loader resolves a module on first use while the capability checks stay exactly the checks the eager path runs, the prewarm hook warms the user chosen set on startup, and every resolution records load telemetry for startup analysis.
|
|
848
|
+
* Lazy loading never hides a capability: a module whose declared capability stays ungranted never resolves, and no budget ever refuses a load because every budget stays the user's choice.
|
|
849
|
+
*/
|
|
850
|
+
|
|
851
|
+
/** The catalog of lazy modules: the heavy parsers and the capture, compare and export families load lazily behind their first command while the startup path stays free of them. */
|
|
852
|
+
export function lazymodcatalog(): lazymoddescriptor[] {
|
|
853
|
+
return [
|
|
854
|
+
{
|
|
855
|
+
id: "capture",
|
|
856
|
+
reason: "the capture family loads behind its first reviewed capture command",
|
|
857
|
+
capabilities: ["capture"],
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
id: "compare",
|
|
861
|
+
reason: "the outputcompare family loads behind its first comparison command",
|
|
862
|
+
capabilities: ["compare"],
|
|
863
|
+
},
|
|
864
|
+
{ id: "export", reason: "the exporttools family loads behind its first export command", capabilities: ["export"] },
|
|
865
|
+
{ id: "htmlsnapshot", reason: "the heavy html parser loads behind its first parse task", capabilities: ["parse"] },
|
|
866
|
+
{ id: "readertree", reason: "the heavy reader parser loads behind its first parse task", capabilities: ["parse"] },
|
|
867
|
+
{ id: "tablerows", reason: "the heavy table parser loads behind its first parse task", capabilities: ["parse"] },
|
|
868
|
+
{ id: "a11ytree", reason: "the heavy a11y parser loads behind its first parse task", capabilities: ["parse"] },
|
|
869
|
+
{ id: "diffpreview", reason: "the heavy diff parser loads behind its first diff command", capabilities: ["parse"] },
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Reads one lazy module descriptor by id; an unknown id returns undefined so the caller keeps its eager fallback. */
|
|
874
|
+
export function lazymodof(id: string): lazymoddescriptor | undefined {
|
|
875
|
+
return lazymodcatalog().find((descriptor) => descriptor.id === id);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Resolves one lazy module on first use: the declared capabilities must all stay granted or the resolution records its refusal, and the load telemetry records the duration and the provenance of the first use. */
|
|
879
|
+
export function resolvelazymod(input: {
|
|
880
|
+
id: string;
|
|
881
|
+
granted: string[];
|
|
882
|
+
firstuse: boolean;
|
|
883
|
+
duration: number;
|
|
884
|
+
now: number;
|
|
885
|
+
provenance?: { runid?: string; stepid?: string; surface?: string };
|
|
886
|
+
}): lazyloadrecord {
|
|
887
|
+
const descriptor = lazymodof(input.id);
|
|
888
|
+
if (descriptor === undefined)
|
|
889
|
+
return {
|
|
890
|
+
id: randomid(),
|
|
891
|
+
moduleid: input.id,
|
|
892
|
+
reason: "the module id names no lazy module",
|
|
893
|
+
resolved: false,
|
|
894
|
+
duration: input.duration,
|
|
895
|
+
at: input.now,
|
|
896
|
+
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
897
|
+
};
|
|
898
|
+
const missing = descriptor.capabilities.filter((capability) => !input.granted.includes(capability));
|
|
899
|
+
return {
|
|
900
|
+
id: randomid(),
|
|
901
|
+
moduleid: descriptor.id,
|
|
902
|
+
reason: descriptor.reason,
|
|
903
|
+
resolved: missing.length === 0,
|
|
904
|
+
duration: input.duration,
|
|
905
|
+
at: input.now,
|
|
906
|
+
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Prewarms the user chosen lazy module set on startup: every prewarmed module resolves immediately with its telemetry recorded while the modules outside the set stay out of the startup path. */
|
|
911
|
+
export function prewarmmodules(input: {
|
|
912
|
+
prewarmset: string[];
|
|
913
|
+
granted: string[];
|
|
914
|
+
now: number;
|
|
915
|
+
duration?: number;
|
|
916
|
+
}): lazyloadrecord[] {
|
|
917
|
+
return input.prewarmset.map((id) =>
|
|
918
|
+
resolvelazymod({
|
|
919
|
+
id,
|
|
920
|
+
granted: input.granted,
|
|
921
|
+
firstuse: false,
|
|
922
|
+
duration: input.duration ?? 0,
|
|
923
|
+
now: input.now,
|
|
924
|
+
provenance: { surface: "startup" },
|
|
925
|
+
}),
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Builds the startup module budget view: the prewarmed modules lead the startup path, the lazy rest stays out of it, and the user configured budget reports over or under without ever refusing a load. */
|
|
930
|
+
export function startupbudgetof(input: { prewarmset: string[]; budget?: number }): startupbudget {
|
|
931
|
+
const catalog = lazymodcatalog();
|
|
932
|
+
const prewarmed = catalog
|
|
933
|
+
.filter((descriptor) => input.prewarmset.includes(descriptor.id))
|
|
934
|
+
.map((descriptor) => descriptor.id);
|
|
935
|
+
const lazy = catalog
|
|
936
|
+
.filter((descriptor) => !input.prewarmset.includes(descriptor.id))
|
|
937
|
+
.map((descriptor) => descriptor.id);
|
|
938
|
+
return {
|
|
939
|
+
prewarmed,
|
|
940
|
+
lazy,
|
|
941
|
+
total: catalog.length,
|
|
942
|
+
...(input.budget !== undefined ? { budget: input.budget } : {}),
|
|
943
|
+
over: input.budget !== undefined && prewarmed.length > input.budget,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** Reads the startup cost the prewarm set adds: the module count the startup path carries beside the lazy rest, reported to the user with no engine ceiling. */
|
|
948
|
+
export function startupcostof(budget: startupbudget): { startupmodules: number; lazy: number; note: string } {
|
|
949
|
+
return {
|
|
950
|
+
startupmodules: budget.prewarmed.length,
|
|
951
|
+
lazy: budget.lazy.length,
|
|
952
|
+
note:
|
|
953
|
+
budget.budget !== undefined && budget.over
|
|
954
|
+
? `The prewarm set exceeds the user startup budget of ${budget.budget}; the view reports the overrun while every load stays allowed.`
|
|
955
|
+
: `The startup path carries ${budget.prewarmed.length} prewarmed module${budget.prewarmed.length === 1 ? "" : "s"} while ${budget.lazy.length} lazy module${budget.lazy.length === 1 ? "" : "s"} wait for their first use.`,
|
|
956
|
+
};
|
|
957
|
+
}
|