Skip to content

perf(e2e): adopt a pre-baked toolkit venv instead of provisioning per suite - #470

Draft
tkislan wants to merge 1 commit into
mainfrom
perf/e2e-prebaked-venv
Draft

perf(e2e): adopt a pre-baked toolkit venv instead of provisioning per suite#470
tkislan wants to merge 1 commit into
mainfrom
perf/e2e-prebaked-venv

Conversation

@tkislan

@tkislan tkislan commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai ignore

Profiling a full E2E run (32039112714) showed the runtime is almost entirely setup, not testing:

18 min total  =  2m08s CI setup  +  15m56s mocha
mocha 948s    =  837s before() hooks (88%)  +  ~111s test bodies (12%)
837s setup    =  402s venv provisioning  +  ~435s per-suite workspace setup

Four suites each created a differently-named Deepnote environment, and every distinct name means another venv plus a full deepnote-toolkit pip install:

Suite Env name before()
initNotebookRunner E2E Init Env 197s
environment #1 E2E Hello Env 121s
environment #2 E2E Delete Env 84s
integrationsEnvFileInjection E2E Integrations Env not yet profiled
helloWorld / snapshots reused E2E Hello Env 36s / 55s

The 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_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. This is what stops the drift recurring.

Bake the venv once. build/e2e/prepareE2eVenv.js creates .venv-e2e with the exact set deepnoteToolkitInstaller installs, reading DEEPNOTE_TOOLKIT_VERSION from source so it cannot drift. 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.

Adopt it instead of building one. createEnvironment now selects the interpreter deterministically rather than selectQuickPick(0). getVenvPathIfInVenv makes the extension adopt any interpreter already inside a venv, and ensureVenvAndToolkit returns 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 }). deleteEnvironment removes 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/cache only writes on a miss, and the key was a bare content hash that kept hitting:

Cache hit occurred on the primary key pip-Linux-py312-2007778…, not saving cache.

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 carries github.run_id, with restore-keys falling back to the newest matching entry, so each run starts warm and saves a refreshed copy.

Expected impact

mocha total
Before 15.8 min 18 min
After ~11 min ~13 min

Projected from measured per-suite costs, not observed — see below.

Verification

Done: npm run compile-e2e exits 0; the workflow YAML parses and step order is correct; the toolkit-version regex resolves 2.1.1 against the real source; settings generation produces valid JSON. npm run lint covers src only, 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 with no interpreter under .venv-e2e was offered and 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 openFolderViaDialog reloading 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

… 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

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0%. Comparing base (aff145f) to head (2bca975).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@     Coverage Diff     @@
##   main   #470   +/-   ##
===========================
===========================
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant