perf(e2e): adopt a pre-baked toolkit venv instead of provisioning per suite - #470
Draft
tkislan wants to merge 19 commits into
Draft
perf(e2e): adopt a pre-baked toolkit venv instead of provisioning per suite#470tkislan wants to merge 19 commits into
tkislan wants to merge 19 commits into
Conversation
… suite
Profiling a full E2E run (CI 32039112714) showed 88% of the 15.8 min mocha
phase is before() hooks, not assertions: 837s of setup against ~111s of test
bodies. 402s of that setup was provisioning Python venvs, because four suites
each created a differently-named Deepnote environment and every distinct name
means another venv plus a full deepnote-toolkit pip install.
Environments are global (globalState + globalStorageUri/deepnote-venvs), so a
shared name is safe across suites regardless of which workspace is open, and
the sharing pattern already existed — two suites carried a "shared env so CI
provisions one venv" comment. initNotebookRunner (197s, the single most
expensive suite) and integrationsEnvFileInjection had each drifted onto their
own name.
Rather than keep fixing that by hand, the name now lives in one constant and
the venv itself is baked once:
- SHARED_ENV_NAME in test/e2e/helpers/constants.ts, imported by all five
sharing suites. 'E2E Delete Env' stays a literal — that suite deletes what it
creates, so it must not share.
- build/e2e/prepareE2eVenv.js bakes .venv-e2e with the exact set
deepnoteToolkitInstaller installs, reading DEEPNOTE_TOOLKIT_VERSION from
source so it cannot drift. It is idempotent and self-healing: a venv that
cannot import deepnote_toolkit is discarded and rebuilt, which also covers a
restored cache whose base interpreter moved.
- createEnvironment now selects the interpreter deterministically instead of
selectQuickPick(0). getVenvPathIfInVenv makes the extension adopt any
interpreter already inside a venv, and ensureVenvAndToolkit returns early once
the toolkit imports, so adopting the baked venv skips creation and pip
entirely. A missing venv warns loudly and falls back, keeping the run slow
rather than red.
- The deletion suite opts out via createEnvironment(name, { useManagedVenv:
true }). deleteEnvironment only removes the venv directory for managed
environments, so adopting the baked venv there would silently stop exercising
that teardown.
- The interpreter path is only known at run time, so the script also emits
test/e2e/settings.generated.json (base settings + python.venvPath +
python.defaultInterpreterPath) and the extest scripts point at it.
Also fixes the pip cache, which had been frozen since its first save:
actions/cache only writes on a miss, and the key was a bare content hash that
kept hitting. Three of the four installed specs are unpinned and installed with
--upgrade, so newer wheels were downloaded every run and never written back.
The key now carries github.run_id with restore-keys falling back to the newest
matching entry, so each run starts warm and saves a refreshed copy.
Verified: tsc (compile-e2e) exits 0, the workflow YAML parses and its step order
is correct, the toolkit-version regex resolves 2.1.1 against the real source,
and settings generation produces valid JSON. NOT verified: no E2E run has
executed against this. The behavioural assumption that needs CI to confirm is
that the Python extension surfaces the baked venv in the interpreter quick pick
via python.venvPath.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #470 +/- ##
===========================
===========================
🚀 New features to boost your workflow:
|
… the row
The deletion suite failed on the first CI run of this branch:
ElementClickInterceptedError: element ... is not clickable at point
(601, 101). Other element would receive the click: <p>...</p>
at QuickPickItem.select (page-objects/.../Input.js:276:9)
at selectInterpreter (out/e2e/helpers/deepnoteEnvironment.js:58:5)
QuickPickItem.select() is a bare click(), and a quick-pick row's description
<p> overlaps the row and swallows positional clicks. selectEnvironmentForNotebook
already documents exactly this and works around it by typing and pressing Enter;
the new managed-venv branch reached for select() and walked straight into it.
The baked-venv branch was unaffected because it already filters by typing. Only
the managed branch needs positional selection, since "any interpreter that is not
the baked venv" cannot be expressed as a filter string — the baked venv's path
contains /bin/python too, so every candidate filter also matches it.
Now walks the highlight with ARROW_DOWN and accepts with ENTER, both sent through
driver.actions() so the arrows and the Enter share one focus context and the
highlight cannot shift in between.
Confirmed from the same run that the rest of the change works: the baked venv was
adopted (zero "no interpreter under .venv-e2e was offered" warnings), the bake
took 66s on a cold cache, and 53 tests passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
…ct id Each of the 20 describes opened its own temp folder, and opening a folder reloads the workbench. That reload — plus the dialog driving around it, which re-clicks OK every 400ms because the simple dialog navigates one level per click — was ~435s of the 837s spent in before() hooks. All fixture copies now live in subdirectories of one root that rootHooks opens once; openFolderViaDialog short-circuits for anything already inside it, so the suites keep calling it and 19 of the 20 reloads disappear. Quick Open stays unambiguous even though marketing-overview.deepnote is used by seven suites, because every describe already removed its own temp directory in after() (20 describes, 20 cleanup calls) — only one suite's subdirectory exists at a time. Losing the reload loses the isolation it provided, so each copy's project id is rewritten to a fresh one. That is what the cross-suite caches key on: the notebook manager (keyed projectId -> notebookId), the tree's groupItemCache, and the exact (projectId, notebookId) lookups in the file watcher and snapshot service. Fixtures deliberately share ids across families — seven suites use project eeee…, three use bbbb… — so without this, one suite's cached project would answer for the next one's freshly copied file. Notebook and block ids are deliberately left alone: suites assert on them directly (snapshots keys off 'e-nb-overview'), and a unique project id already makes every (projectId, notebookId) pair unique. Details worth knowing: - The rewrite is per copy *set*, not per file. Seven suites build sibling sets with raw fs.copyFileSync, which would have split one project in two — siblings now go through copyFixtureIntoDir and share the directory's mapping. A set that mixes projects on purpose (explorerGrouping sits marketing siblings next to quick-notes and asserts two groups) still maps distinct ids distinctly. - Snapshot filenames encode the project id (buildSnapshotPath -> generateSnapshotFilename), so copySnapshotIntoDir renames the file in lockstep. Without it the snapshot is simply never found — no error, just a missing output. - Not every fixture uses a uuid: hello-world and integrations-env-file use slugs like e2e-hello-world-project. Fresh ids keep the shape and deliberately share no prefix with the id they replace, and replacement refuses to match when the id is only the start of a longer one. - environment and statusBar asserted committed project ids; both now read copy.projectId. Verified: tsc exits 0, and a harness exercising the compiled helpers against all 13 committed fixtures checks that every project id is rewritten and unique, that siblings inherit the family id, that a deliberately different project stays separate, that snapshot content and filename are rewritten together, that two copies of one fixture get different ids, and that cleanup removes the directory. NOT verified: no E2E run yet — the reload removal and the shared workspace are behavioural and need CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
The E2E job ran all 17 suites in one sequential mocha process. Splitting it across a matrix cuts wall time, but a split has to be maintainable: a list of files balanced by measured duration goes stale the moment someone adds a suite, and the way it goes stale is invisible. Grouped by what the suites cover instead, one directory per shard: kernel/ 4 running code and environments files/ 6 the .deepnote file lifecycle — split, create, rename, delete workspace/ 7 surfacing notebooks and reacting to changes on disk A new suite has an obvious home, and the weights happen to land close enough (~150s / ~100s / ~110s against the post-venv-share profile) that no shard is much more than 1.5x another. Balance is a consequence of the grouping rather than something to re-tune. Each shard pays the ~2m08s of setup again, which is what caps the return: three shards is where the ratio peaks, and past four the setup dominates. check:e2e:groups guards the failure mode that matters. A suite in the wrong place does not fail anything — it silently never runs, which looks like a faster green build. The check fails when a suite sits outside a group directory, when a group has no matching npm script, when a group is not named in the workflow, and when a script has no directory. Verified it exits 1 on a stray suite and 0 once moved back. Screenshot artifacts are per shard, since three jobs uploading one name collide. Verified: tsc exits 0 after the move, the compiled tree matches the shard globs (4/6/7), the workflow parses with the expected matrix, and the guard passes. NOT verified: no E2E run yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
The group names were written twice — in the matrix and, indirectly, in the check that ran inside every shard. YAML anchors would be the natural fix but GitHub Actions does not support them (actions/runner#1182), and a workflow-level `env` does not help either since `env` is not a permitted context inside `strategy`. A job output is the one mechanism that lets two jobs read the same value, so a `groups` job now declares the list and both the matrix and the verification job consume it. The check also moves out of the shards into its own `verify-groups` job. It needed neither VS Code nor the VSIX, so running it three times behind a full setup was waste; it now runs once on a bare checkout and reports as its own status. checkSuiteGroups.js takes the authoritative list from E2E_GROUPS — the same job output the matrix reads, so the two cannot drift — and falls back to inferring it from the `test:e2e:<group>` scripts when run locally. It fails when a suite sits outside a group directory, when a suite directory is absent from the list (the case this job exists for: nothing runs it and the build just goes green sooner), when a listed shard has no directory or no suites, and when a shard has no npm script. Cost is one extra job on the critical path, roughly 15s of scheduling before the shards start. Verified all four states: green with the list inferred locally, green with E2E_GROUPS set, red naming test/e2e/suite/workspace/ when the list omits it, and red naming a listed shard with no directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
Throwaway workflow, workflow_dispatch only so it never runs by itself. GitHub added YAML anchor support in September 2025, but no source states whether the workflow schema accepts a custom top-level key to hold one, and the parser rejects unknown top-level keys in general. Pushing this asks GitHub directly without risking e2e.yml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I claimed GitHub Actions does not support YAML anchors. That was wrong — support shipped 2025-09-18; the runner issue I cited had been closed by it. The previous commit's `groups` job existed only to work around a limitation that no longer exists, so it is gone. Two constraints do survive, and they decide the placement: - Merge keys (`<<:`) are still unsupported, so an anchor can only be aliased whole. - A custom top-level key to hold the anchor is rejected. Probed it directly rather than guessing: a throwaway workflow with `x-e2e-groups: &e2e-groups [...]`, dispatch-only so it could not run on its own, still produced a push-triggered run with zero jobs and no logs — GitHub's signature for an invalid workflow file. So the list cannot sit at the top of the file; it is declared at its first use, the earliest the schema allows. The list is now written once, anchored on the shard matrix. verify-groups reads it back out of the workflow with js-yaml, which resolves the anchor — so the check runs against the list the shards actually use rather than a copy of it. That also drops the E2E_GROUPS plumbing; the variable survives only as the seam that lets the failure paths be exercised without editing the workflow. verify-groups no longer gates the shards (no `needs`), so a grouping mistake reports as its own red status while the E2E results still come through. It installs with --omit=dev since js-yaml is a production dependency. Verified: the matrix resolves to [kernel, files, workspace] through the anchor, the check passes reading it from the workflow, and both failure paths still name the offender — a suite directory outside the matrix, and a shard with no directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
The previous two attempts were both worse than the problem. A job output existed only to route around a limitation that had been lifted; then an anchor that could not be placed where it belonged, paired with a checker that parsed the workflow to read its own matrix. Both were elaborate ways to keep two copies of one list in agreement. There is no second copy now. A discover job lists the directories under test/e2e/suite/ and emits them as an output; the matrix is that output. A new group directory gets a shard on its next run, and no directory can be left unrun, because the listing is the list. That removes the anchor, the workflow self-parsing, checkSuiteGroups.js, the verify job, and the three per-group npm scripts — which were themselves a second list. The shard runs the glob directly, so the group name appears in exactly one place. Artifacts would have been needed for the post-check as sketched: matrix legs all write the same outputs map and the last writer wins, so a shard cannot report its own name back. Deriving the matrix instead makes the comparison unnecessary rather than making it work. The two things still worth failing on are cheap and stay in the discover job: a suite outside a group directory, and a group directory with no suites. Both would otherwise pass silently — nothing runs them and the build goes green sooner. Verified: the scan emits ["files","kernel","workspace"] against the real tree, and on throwaway trees it exits 1 naming loose.e2e.test.ts for a stray suite and naming hollow/ for an empty group. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
Keeps the matrix an explicit list, as it was to begin with, and checks it against the directories after the fact rather than deriving one from the other. Each shard touches a file named after its group and uploads it; verify-coverage downloads them all and fails for any test/e2e/suite/*/ directory that no shard reported. Adding a directory without adding it to the matrix is the mistake this catches, and it is worth catching because nothing runs it — the build just goes green sooner. Artifacts rather than job outputs because matrix legs share one outputs map and the last leg to finish wins, so a leg cannot report its own name that way. The record is written with `if: always()`, so a shard whose tests failed still counts as having run — otherwise a failing shard would also be reported as an uncovered directory. The job itself runs on `!cancelled()` rather than `always()` because this workflow sets cancel-in-progress, and on a superseded run `always()` would report every directory as unrun. Verified the comparison on the real directory tree: green when all three shards report, and red naming test/e2e/suite/workspace/ when only two do. Also confirmed the stray-suite branch fires, and that with no records at all every directory is reported — the case the cancellation guard exists to keep out of CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
Every shard failed with 0 passing: 5 suites in kernel, 10 in workspace, each timing out on "<fixture>.deepnote did not open" or, for explorerGrouping, on the project group never appearing. The shared fixtures root was never opened. ExTester's runner does not fire `mochaHooks.beforeAll` — the log puts "Launching tests..." at 15:19:33.6461639 and the first suite title at 15:19:33.6553243, 9ms later, while that hook does a waitForWorkbench and drives a folder dialog. It returned without running, so VS Code had no workspace folder at all. Quick Open then matched nothing, confirmed an empty result, and no editor opened — which surfaces as a timeout waiting for the editor rather than as a failure to find the file, so the errors pointed away from the cause. openFolderViaDialog now opens the root itself, on the first request for anything inside it, and no-ops afterwards. The one reload still happens once, and it no longer depends on a hook that never runs. rootHooks keeps only its afterEach, with a note about why nothing else can live there. removeFixturesWorkspaceRoot went with the root hook that called it. Each suite already removes its own subdirectory in `after`; what is left behind is one empty mkdtemp directory per run. Verified: tsc exits 0 and the fixture harness still passes all six checks. NOT verified: this needs a CI run — the failure it fixes was only observable there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
All three shards now pass locally: kernel 7, files 25 (+1 pending), workspace 23. CI had kernel 3/3 and files 20/2. Sharing one workspace removed the reload between suites, and the reload had been doing more than opening a folder. Four things it hid: - Editors leak. A suite that renames or deletes notebooks leaves tabs open on files that no longer exist, so the next suite starts with the wrong notebook active and finds no code cell in it — projectRename opened onto a struck- through marketing-overview-copy.deepnote left by notebookCommands. Every suite's own after() already called EditorView.closeAllEditors, but that clicks each tab's close button and fails on notebook tabs with ElementNotInteractableError, swallowed by the surrounding .catch. Closing via the palette works, and doing it centrally fixes every suite at once. - The Deepnote Explorer keeps a group for a directory that has been removed. Two identical "Bootstrap Only" groups were visible, and findDeepnoteLeaf took the dead one, so the delete confirmation never appeared. - The env sidecar is written to workspace.workspaceFolders[0] (deepnoteExtensionSidecarWriter.node.ts:300), which is now the shared root rather than the suite's directory, so the test read a path that never exists. Its mappings are keyed by project id and every copy gets a fresh one, so a shared file still isolates suites. - Selecting a non-baked interpreter by walking the list with arrow keys landed on the wrong entry, leaving the deletion suite without a managed environment: env creation timed out and its kernel never bound. It now filters to the wanted interpreter and accepts with Enter, the same mechanism the baked-venv branch uses, and warns instead of silently picking something else. The editor close and tree refresh live in openFolderViaDialog's no-op branch — every suite already calls it in before(), so it is where a suite starts. Also: .venv-e2e is excluded from the VSIX. Packaging pulled in 28846 files / 740MB and failed on a symlink; CI only escaped because it packages before the bake step. Breadcrumbs are off in the E2E settings — they sit directly above the notebook toolbar, and every run verified here had them disabled. Verified by running each shard locally under xvfb against a real VSIX and a clean VS Code profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
…npm scripts Suites had drifted into two flows. Each called openFolderViaDialog(tempDir), but that function no longer opened the folder it was handed — it inspected the path, opened the shared root on the first call, and on later calls quietly did cleanup instead. The name described one thing and the code did another depending on which suite got there first. Now there is one entry point. Every suite calls enterFixturesWorkspace() and gets identical behaviour: the shared workspace open, no editors left by the previous suite, and a Deepnote Explorer that matches disk. openFolderViaDialog goes back to being a private primitive that opens exactly the folder it is given, and isInsideFixturesWorkspaceRoot is gone — with a single entry point there is no path to classify. The E2E job runs `npm run test:e2e:ci` instead of an inline npx invocation, so the flags live in package.json with the other test scripts and the shard comes from E2E_GROUP. All three test:e2e scripts now run setup:e2e:venv first. Without it a fresh clone failed on a missing settings.generated.json, since that file is written by the bake step — running the tests now provisions what they need. The separate CI bake step is gone for the same reason: one way the venv gets made. Verified after the refactor by running each shard locally under xvfb: kernel 7, files 25 (+1 pending), workspace 23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
The venv was provisioned by a build script that also wrote a generated settings file, because the interpreter had to be named by absolute path and that path is only known at run time. Settings are read when VS Code launches, so the script had to run before the tests — a fresh clone that skipped it failed on a missing file, and CI needed a step whose only job was to run it. ensureManagedVenv() replaces all of that. It creates the venv if it is missing or cannot import the toolkit, and is otherwise a single `import deepnote_toolkit` — cheap enough for suites to call unconditionally. Creating the shared workspace links the venv into it as `.venv`, which is where the Python extension looks with no configuration at all. So there is nothing to run first. Tests provision what they need, and the venv stays at a fixed path that CI can cache. The workflow step is now only about where the time is spent — it calls the same function, so skipping it would make the run slower rather than break it. Removed by this: build/e2e/prepareE2eVenv.js, settings.generated.json and its gitignore entry, the python.venvPath / python.defaultInterpreterPath injection, and the `npm run setup:e2e:venv &&` prefix on the test scripts. Net 116 lines deleted against 26 added. Verified locally under xvfb, all three shards green with no settings file present: kernel 7, files 25 (+1 pending), workspace 23. The load-bearing assumption — that the Python extension finds a `.venv` link inside the workspace without configuration — was tested on its own first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
tkislan
force-pushed
the
perf/e2e-prebaked-venv
branch
from
August 21, 2026 13:14
bdf48da to
b6958ee
Compare
…k cheap Two things the E2E venv helper was getting wrong on its own. It duplicated the extension's pip specs. The installer cannot be reused directly — it is an injected class built on workspace.fs, l10n and cancellation tokens, running in the extension host, while this runs in the Mocha process where `vscode` does not resolve; importing from src/ drags that whole graph in. But the *spec* can be shared, so DEEPNOTE_TOOLKIT_PACKAGES now sits next to DEEPNOTE_TOOLKIT_VERSION, the installer spreads it, and the helper reads both from that one file. A package added for production can no longer leave the tests provisioning an environment production never has. Worth recording: the first attempt at reading that list back was wrong in exactly the way this guards against. `\[([^\]]*)\]` stopped at the `]` inside 'python-lsp-server[all]' and silently yielded a two-package list, which would have under-provisioned the venv without failing anything. Asserting the derived list against the installer's caught it. The health check also cost 14s per run. It used `import deepnote_toolkit`, which executes the whole package and retries network calls to a userpod API that is not there, dumping a page of warnings into every log. Reading installed distribution metadata gives the same guarantee — right packages, right versions — in 0.15s and silently. Verified: tsc exits 0, 2612 unit tests pass, the derived spec matches the installer's list exactly, and the check still fails on a wrong version and on a missing package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
`prompt.takeAction(SPLIT_ACTION)` used a Notification captured several steps earlier, with a screenshot in between. The toast re-renders while it sits there, so the reference goes stale and the hook dies with StaleElementReferenceError — reproduced on two consecutive runs, always after 02-split-prompt and never reaching 03-split-done. It failed the whole describe rather than one test, because mocha's `retries: 1` covers tests but not `before all` hooks. Now locates and acts inside one wait loop, the same shape clickRunAll already uses for the notebook toolbar and for the same reason. Verified: the test passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
… piecemeal Sharing one workspace removed the per-suite window reload, and the reload turned out to be isolating more than it looked: open editors, the Deepnote Explorer's cached project groups, project data keyed by id, and the Python extension's interpreter discovery. Each was fixable on its own, and each fix uncovered the next — editors, then the tree, then discovery. That is not a list that ends. So the reload comes back, but the expensive part does not. Opening a folder was never slow because of the reload; it was slow because `File: Open Folder...` navigates one level per OK click in the simple dialog, so reaching a path meant re-clicking for up to FOLDER_OPEN_TIMEOUT. Keeping one workspace and reloading inside it buys the isolation without the dialog. Editors are closed before the reload rather than after: a reload restores what was open, so a suite that renamed or deleted notebooks would otherwise hand its dead tabs straight to the next one. The explorer refresh is gone — a fresh extension host re-scans. Not measured locally. This machine has degraded through a full disk, a venv built against that full disk, and orphaned toolkit servers left by killed runs, and it no longer produces a runtime number worth quoting. CI gets a clean machine per run and three shards in parallel, which is where the comparison belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
kernel was the long pole at 10m10s while files and workspace both finished around
7m, so the whole run waited on it. Its 7 minutes of tests split cleanly along what
the suites actually cover:
environments/ 155s the environment lifecycle — create, select, migrate on
split, delete and tear the server down
execution/ 266s running code in a kernel — hello world, the sibling init
notebook, integration env vars reaching the process
Measured from the last CI run rather than guessed: initNotebookRunner alone is
170s (40% of the old shard), and environment's two describes are 84s and 71s.
Balance is a consequence of the split rather than its purpose, but it lands
reasonably: the heavier half is 266s against 155s, and since roughly 3.1m of each
job is fixed setup, the expected long pole drops from 10m10s to about 7.6m.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
…cript Most of these explained a decision that the code or the surrounding config already makes obvious, or recorded how something got here rather than what it does. Kept the ones carrying context from outside the file: why the pip cache key includes run_id, why the venv cache is keyed on the Python version, and why the toolkit spec is read from source instead of imported. Two were stale rather than merely long. selectInterpreter still pointed at build/e2e/prepareE2eVenv.js, deleted when ensureManagedVenv replaced it. test:e2e:prebuilt had no callers: CI runs test:e2e:ci and local runs test:e2e, whose setup-and-run skips the VS Code download when it is already present. Verified: tsc and compile-e2e exit 0, the workflow parses with the same four shards, and setup:e2e:venv still runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
The rewriting was added when the suites shared one window with no reload between them, to keep the extension's project-id-keyed caches from carrying one suite's state into the next. Reinstating the per-suite window reload made it redundant: the reload restarts the extension host, which clears those caches, and every suite already removes its own fixture directory in `after`, so no two copies of a fixture are in the workspace at once. Copies keep their committed ids again, so the two suites that asserted on a rewritten id go back to the constants they used before. The one thing the reload does not clear is `.vscode/deepnote.json`, written at the shared root and keyed by project id. Nothing reads it back to drive behaviour, and the suite that asserts on it deletes the file first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@coderabbitai ignore
Profiling a full E2E run (32039112714) showed the runtime is almost entirely setup, not testing:
Four suites each created a differently-named Deepnote environment, and every distinct name means another venv plus a full
deepnote-toolkitpip install:initNotebookRunnerE2E Init Envenvironment#1E2E Hello Envenvironment#2E2E Delete EnvintegrationsEnvFileInjectionE2E Integrations EnvhelloWorld/snapshotsE2E Hello EnvThe two that reused a name cost a fraction of the ones that didn't — the sharing pattern already worked, and two suites had simply drifted off it. Environments are global (
globalState+globalStorageUri/deepnote-venvs), so sharing a name is safe regardless of which workspace is open.Changes
One name, one place.
SHARED_ENV_NAMEintest/e2e/helpers/constants.ts, imported by all five sharing suites.'E2E Delete Env'stays a literal — that suite deletes what it creates, so it must not share. This is what stops the drift recurring.Bake the venv once.
build/e2e/prepareE2eVenv.jscreates.venv-e2ewith the exact setdeepnoteToolkitInstallerinstalls, readingDEEPNOTE_TOOLKIT_VERSIONfrom source so it cannot drift. Idempotent and self-healing — a venv that cannotimport deepnote_toolkitis discarded and rebuilt, which also covers a restored cache whose base interpreter moved.Adopt it instead of building one.
createEnvironmentnow selects the interpreter deterministically rather thanselectQuickPick(0).getVenvPathIfInVenvmakes the extension adopt any interpreter already inside a venv, andensureVenvAndToolkitreturns early once the toolkit imports — so adoption skips venv creation and pip entirely.The deletion suite keeps building its own. It opts out with
createEnvironment(name, { useManagedVenv: true }).deleteEnvironmentremoves the venv directory for managed environments only, so adopting the baked venv there would silently stop exercising that teardown. It stays self-contained and remains the one place real managed creation + teardown is covered.Generated settings. The interpreter must be named by absolute path, known only at run time, so the script also emits
test/e2e/settings.generated.json(base settings +python.venvPath+python.defaultInterpreterPath) and the extest scripts point at it. Gitignored.Also: the pip cache had been frozen since its first save
actions/cacheonly writes on a miss, and the key was a bare content hash that kept hitting:Three of the four installed specs are unpinned and installed with
--upgrade, so newer wheels were downloaded every run and never written back — the entry stayed at whatever existed when the key was last busted. The key now carriesgithub.run_id, withrestore-keysfalling back to the newest matching entry, so each run starts warm and saves a refreshed copy.Expected impact
Projected from measured per-suite costs, not observed — see below.
Verification
Done:
npm run compile-e2eexits 0; the workflow YAML parses and step order is correct; the toolkit-version regex resolves2.1.1against the real source; settings generation produces valid JSON.npm run lintcoverssrconly, so nothing here is linted by CI.Not done — no E2E run has executed against this. The behavioural assumption that needs CI to confirm is that the Python extension surfaces the baked venv in the interpreter quick pick via
python.venvPath. If it does not, the helper warns withno interpreter under .venv-e2e was offeredand falls back to the old behaviour, so the run stays green and merely slow. Check the E2E log for that warning before trusting the numbers.Follow-up, not in this PR
The remaining ~435s of setup is
openFolderViaDialogreloading the workbench once per suite, across 17 suites at 14–55s each. After this change that is the dominant cost, and it is a fixture restructure rather than a caching problem.🤖 Generated with Claude Code
https://claude.ai/code/session_01MyZCw9GzL6Vq7S9MAVYwby