perf: render init templates in the engine, and fix two module names that could not load - #16
Draft
eunomie wants to merge 11 commits into
Draft
perf: render init templates in the engine, and fix two module names that could not load#16eunomie wants to merge 11 commits into
eunomie wants to merge 11 commits into
Conversation
Records a measured baseline for the three surfaces this module owns (init, generate, and its contribution to the call path), captured from local engine traces rather than intuition, plus the improvements that follow from it. Signed-off-by: Yves Brissaud <yves@dagger.io>
renderedTemplate pulled golang:1.25-alpine, compiled helpers/render-template
from source, and ran it — to substitute three variables across a handful of
small text files. On a cold engine that dominated 'dagger module init python'.
Render in Dang instead: walk the template with glob, expand {{ .Var }} actions
in both paths and contents, and carry non-.tmpl files over as files so their
bytes and mode survive. Unknown variables raise rather than rendering empty.
camelName and splitWords reproduce the two distinct conversions the Go helper
took from strcase: ToCamel splits on separators only and lower-cases a letter
following another upper-case letter, while ToSnake splits camelCase humps and
keeps runs of capitals together. Output was verified byte-identical to the Go
helper across my-module, my_module, myModule, HTTPServer and simple, for each
of the default, empty and legacy templates.
ModuleImport is dropped: no Python template uses it, and an unknown variable
now raises, which documents its absence at the point of use.
Signed-off-by: Yves Brissaud <yves@dagger.io>
The rendering assertions were all contains-only, so an extra, missing, or misnamed output file passed. Assert the exact file set for the default and legacy templates instead, which also covers the legacy template's .gitignore and .gitattributes — non-template files that no check referenced before. Add a naming check over every spelling a user might pass to init. The type and package names come from two different conversions, and HTTPServer is the case where they visibly disagree (Httpserver / http_server); pinning it keeps that behaviour from drifting silently. Signed-off-by: Yves Brissaud <yves@dagger.io>
The helpers pinned golang:1.25-alpine while the polyfill dependency's Go runtime pulls golang:1.26-alpine, so a cold engine pulled two Go base images to run 'config get', 'config set', or an init with a non-default flag. Use the same image for both; helpers/pyproject declares go 1.25.0, which 1.26 builds. Signed-off-by: Yves Brissaud <yves@dagger.io>
The default template's create() returns the class it is declared in. Deferred annotations only became the default in 3.14 (PEP 649), so on anything older the forward reference is evaluated eagerly and the module fails to load at all: dagger module init python app --python-version 3.13 dagger call app container ModuleLoadError: name 'App' is not defined That is a documented init flag producing a module that cannot be loaded, and config set --python-version reaches the same state. Import annotations from __future__ so the reference stays a string on every supported version. Verified by initializing, generating and calling a module at 3.12, 3.13 and the 3.14 default. The import is free: repeated interleaved call benchmarks put it within noise of the template without it (-13 ms and +10 ms across two batches). Signed-off-by: Yves Brissaud <yves@dagger.io>
Rewrites the baseline against what the numbers and the reviews actually support: the init win is ~1.2s on a cold engine and nothing on a warm one, the polyfill commit-pin idea is dropped as a proven no-op, and the call-path section replaces 'interpreter boot' with the measured split — interpreter start is 8ms of a 1050ms process; the import chain, the first-connect handshake and per-call bytecode recompilation are the real costs, all upstream. Records the runtime-config sweep including the two arms deliberately not adopted: python:3.14-alpine costs +24% per call, and use-uv=false saves 12% by a mechanism nobody could isolate. Signed-off-by: Yves Brissaud <yves@dagger.io>
`.ModuleName` was only ever read through pyproject.toml, and nothing asserted that file was expanded, so a renderer regression that left it untouched passed every check. Assert the rendered name and the absence of a leftover action. Add `from __future__ import annotations` to the default template assertions: dropping it from the template silently returns `init --python-version 3.13` to producing a module that cannot be loaded. Add an `s3-bucket` row to the naming check. It is the case the deleted Go helper got wrong — `strcase` split on the digit and rendered `src/s_3_bucket/`, which never matches the package `uv_build` derives from the project name. While here, key each row's output path on the input name: kebab, snake and camel spellings all shared one path and only the last write was observed. Signed-off-by: Yves Brissaud <yves@dagger.io>
Both helper containers pinned `golang:1.26-alpine` as a bare literal, with nothing recording that the value tracks the image polyfill's own Go helpers pull. Bind it once so the coupling is stated where it can be read. Signed-off-by: Yves Brissaud <yves@dagger.io>
The entry still pinned `golang:1.25-alpine`, so a frozen arm64 run had no lock entry for the image the code asks for. Digest resolved from the tag as dagger resolves it. Signed-off-by: Yves Brissaud <yves@dagger.io>
Fix the SHA cited for the Python 3.14 template fix, and record the digit-name bug the renderer also fixed: `init` produced an unloadable module for any name containing a digit, so the naming output diverges from `strcase` on purpose. Correct the cross-SDK claim — go-sdk's copy of the helper had already forked and sdk-sdk's is a different tool — and point consolidation at a Dang `renderTemplate` primitive rather than another shared Go helper. Record the template features the substituter dropped, qualify the file-mode equivalence claim, scope goal 1 to this module's toolchain, state how the cold-init A/B was ordered, and list what the review left deliberately unfixed. Signed-off-by: Yves Brissaud <yves@dagger.io>
CI is green on the branch, so the work this doc governs is done. Move it to hack/designs/done/ and record the final state, including the two null results worth keeping: the polyfill commit-pin that provably changes nothing, and the use-uv default that measures faster for reasons nobody could isolate. Signed-off-by: Yves Brissaud <yves@dagger.io>
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.
Trace-driven pass over the three surfaces this module owns:
dagger module init python,dagger generate, and what the SDK contributes todagger call. Baseline, method, raw runs and the ideas that were measured and rejected are all inhack/designs/2026-08-14-python-sdk-performance.md.What changed
Init renders templates in the engine instead of a Go toolchain.
renderedTemplatepulledgolang:1.25-alpine, compiledhelpers/render-templatefrom source, and ran it — to substitute three variables across a handful of small text files. It now walks the template withglob, expands{{ .Var }}in paths and contents, and carries non-.tmplfiles over withwithFile.helpers/render-template/is deleted.One Go base image instead of two. The helpers pinned
golang:1.25-alpinewhile thepolyfilldependency's Go runtime pullsgolang:1.26-alpine, so a cold engine pulled both. The remaining helper (helpers/pyproject, used byconfig get/setand flag-bearinginit) now builds on the image the workspace already has.Two latent bugs fixed, both found while measuring:
dagger module init python app --python-version 3.13produced a module that could not be loaded at all —ModuleLoadError: name 'App' is not defined. The default template'screate()returns the class it is declared in, and deferred annotations only became the default in 3.14 (PEP 649). Fixed withfrom __future__ import annotations; verified by initializing, generating and calling at 3.12, 3.13 and 3.14.dagger module init python s3-bucket— any name containing a digit — was also broken onmain.strcase.ToSnakerenderssrc/s_3_bucket/, which is not whatuv_buildderives fromname = "s3-bucket", so the module failed to load withcall constructor: exit code: 1. The new renderer producessrc/s3_bucket/and the module loads and calls.Honest numbers
Local engine
v1.0.0-beta.9; the host is shared and noisy, so arms were interleaved and every run is recorded in the doc.withInitModulewithInitModuleSo: ~1.2s (≈10%) off a cold init, and nothing measurable when warm — the
go buildlayer was already cached in the warm case. The saving is smaller than the removed work suggests because the helper's build overlapped the polyfill Go build that remains. Expect little or no CI wall-clock change; an earlier draft of the doc predicted a large CI win from check durations and that reasoning was wrong, which the doc now says.The durable wins are as much qualitative as numeric: the default scaffolding path no longer needs a Go toolchain or a Docker Hub round-trip for a Go image, and one of two helper binaries is gone.
Rendered output was diffed against the deleted Go helper's for
my-module,my_module,myModule,HTTPServerandsimpleacross thedefault,emptyandlegacytemplates — 15 combinations, byte-identical. Digit-bearing names deliberately differ, becausestrcase's answer was the broken one.Measured and deliberately not shipped
git ls-remote. It does not work:NewGitRepositorycallsbackend.Remote()unconditionally, and the ref already carries the SHA viarefPin. The fix belongs in the engine; dropped rather than shipped as a no-op.use-uv = falsemeasures −12% per call, reproduced across two independent benchmark batches. Not adopted: every in-container micro-benchmark is identical between the arms, so the ~200ms/process lives somewhere nobody could isolate, and it trades away uv's install speed on a path that was not benchmarked. Recorded for the runtime owners instead.base-image = "python:3.14-alpine"is worth knowing about in the other direction: +24% per call, because musl CPython walks the same import chain in 607ms instead of 453ms.Where the call-path time actually goes
A warm
dagger callis ~3.4s, of which ~2.1s is twoexec.processRunspans in the Python runtime container. Instrumenting inside that container splits one ~1050ms process as: ~450msimport dagger(300ms of it the generatedclient/gen.py), 253ms first-connect handshake, ~200ms container setup/teardown, ~60ms recompiling the vendored SDK to bytecode that is then thrown away, 38ms telemetry init — and ~8ms of actual interpreter start, 0.8% of the total.None of that is fixable here; it lives in
dagger/dagger'ssdk/python/runtime. The three cost centres are written up in the doc for whoever picks them up upstream.Testing
All 35 workspace checks pass locally (13
e2e, 22sdk-sdk). The e2e additions were written to protect this rewrite specifically: exact-file-set assertions (which caught a stray file during development), a naming matrix including the digit andHTTPServercases, an assertion thatpyproject.tomlis actually expanded —.ModuleNameappears nowhere else, so a regression there previously passed every check — and an assertion that the__future__import stays in the template.The doc records what was accepted and not fixed: template symlinks are no longer rejected, empty template directories are dropped, and the template language is narrowed from Go
text/templateto{{ .Var }}substitution only.