Runtime tracing, flight recording#3627
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Go runtime/trace integration (including a flight recorder) to tsgo, and wires capture/control into the LSP + the preview VS Code extension to make runtime tracing accessible similarly to existing pprof tooling.
Changes:
- Introduces
internal/runtimetraceto manage process-lifetime runtime tracing and flight-recorder snapshots (env-driven + SIGUSR1 on Unix). - Plumbs
context.Contextthrough checker/program APIs and adds trace regions/log events across compiler, parser, resolver, project system, and LSP request handling. - Adds new LSP custom requests + VS Code extension commands to start/snapshot/stop the flight recorder.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/transformers/tstransforms/importelision_test.go | Updates test program stub + checker creation to pass context.Context. |
| internal/testrunner/compiler_runner.go | Passes t.Context() into ForEachCheckerParallel for tracing-friendly APIs. |
| internal/runtimetrace/runtimetrace.go | New session management for runtime trace + flight recorder via env vars, snapshot-on-stop, Unix signal hook. |
| internal/runtimetrace/recorder.go | New FlightRecorder wrapper around runtime/trace flight recorder APIs (start/snapshot/stop). |
| internal/runtimetrace/api.go | New helpers for trace regions/tasks and safe/unsafe logging conventions. |
| internal/runtimetrace/signal_unix.go | Unix SIGUSR1 handler to trigger a flight snapshot. |
| internal/runtimetrace/signal_other.go | Non-Unix no-op signal handler implementation. |
| internal/project/snapshot.go | Adds trace region around snapshot cloning. |
| internal/project/session.go | Adds trace region around snapshot updates. |
| internal/project/project.go | Adapts program binding call to new context-aware BindSourceFiles. |
| internal/project/checkerpool.go | Threads ctx into checker creation in the project checker pool. |
| internal/parser/parser.go | Adds trace region + trace log events to parsing entrypoint. |
| internal/module/resolver.go | Adds trace region + trace log events to module resolution entrypoint. |
| internal/lsp/server.go | Adds per-request trace tasks/logging and new handlers for flight recorder control. |
| internal/lsp/lsproto/lsp_generated.go | Generated protocol additions for flight recorder requests/params/results. |
| internal/lsp/lsproto/_generate/generate.mts | Adds generator specs for flight recorder protocol structures/requests. |
| internal/ls/autoimport/util.go | Makes checker pool acquisition context-aware and threads ctx into checker creation. |
| internal/ls/autoimport/registry.go | Updates checker creation/usage to pass ctx and handle canceled acquisition. |
| internal/ls/autoimport/aliasresolver.go | Updates BindSourceFiles signature for ctx-aware checker.Program. |
| internal/execute/tsc.go | Adds trace regions around compilation entrypoints. |
| internal/compiler/program.go | Adds trace regions/logging in binding/diagnostics/emit, and threads ctx through checker pool APIs. |
| internal/compiler/fileloader.go | Adds trace region around program file processing. |
| internal/compiler/emitter.go | Adds trace region/logging for emit and adds additional transformer regions. |
| internal/compiler/checkerpool.go | Threads ctx into checker creation and adds trace region/logging around checker pool init. |
| internal/checker/checker_test.go | Updates tests/bench to new ctx-aware BindSourceFiles/NewChecker. |
| internal/checker/checker.go | Makes Program.BindSourceFiles ctx-aware and adds trace regions/logging in checker creation + file checking. |
| cmd/tsgo/main.go | Starts/stops runtime tracing session for the tsgo process lifetime. |
| _extension/src/session.ts | Adds VS Code commands for start/snapshot/stop flight recorder and context key wiring. |
| _extension/src/client.ts | Adds client methods to call new custom LSP flight recorder requests. |
| _extension/package.json | Registers new extension commands and enablement conditions. |
| .golangci.yml | Updates depguard rules to forbid direct runtime/trace usage outside internal/runtimetrace and adjusts linter exclusions. |
| func (s *Server) handleSnapshotFlightRecorder(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) { | ||
| filePath, err := s.flightRecorder.Snapshot(params.Dir) | ||
| if err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
handleSnapshotFlightRecorder dereferences params.Dir without checking whether params is nil. Since the generated handler signature accepts *ProfileParams, a malformed/null params payload would panic the server. Consider validating params != nil (and returning an InvalidRequest-style error) before using it.
|
|
||
| switch os.Getenv(envRuntimeTraceDetail) { | ||
| case "", "0", "false", "no", "off": | ||
| // detail logging disabled |
There was a problem hiding this comment.
Start only ever sets unsafeLogging to true; it never resets it to false when TSGO_RUNTIME_TRACE_DETAIL is unset/falsey. If Start is called multiple times in-process (tests, multiple entrypoints, etc.), unsafe logging can remain enabled unexpectedly and cause user-data to be emitted even when the env var is off. Consider explicitly storing false in the disabled case (and/or resetting on Stop) so the value always reflects the current configuration.
| // detail logging disabled | |
| unsafeLogging.Store(false) |
| const ( | ||
| envRuntimeTrace = "TSGO_RUNTIME_TRACE" | ||
| envRuntimeTraceFlight = "TSGO_RUNTIME_TRACE_FLIGHT" | ||
| envRuntimeTraceFlightMinAge = "TSGO_RUNTIME_TRACE_FLIGHT_MIN_AGE" | ||
| envRuntimeTraceFlightMaxByte = "TSGO_RUNTIME_TRACE_FLIGHT_MAX_BYTES" | ||
| envRuntimeTraceDetail = "TSGO_RUNTIME_TRACE_DETAIL" | ||
| ) |
There was a problem hiding this comment.
The constant name envRuntimeTraceFlightMaxByte is singular, but it corresponds to TSGO_RUNTIME_TRACE_FLIGHT_MAX_BYTES (plural). Renaming it to ...MaxBytes would avoid confusion and better reflect the actual env var and meaning.
| @@ -67,6 +73,7 @@ func (e *emitter) runScriptTransformers(emitContext *printer.EmitContext, source | |||
| } | |||
|
|
|||
| func (e *emitter) runDeclarationTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, declarationFilePath, declarationMapPath string) (*ast.SourceFile, []*ast.Diagnostic) { | |||
| defer runtimetrace.Region(context.TODO(), "emitter.runDeclarationTransformers")() | |||
There was a problem hiding this comment.
runScriptTransformers/runDeclarationTransformers start trace regions with context.TODO(), which breaks nesting/association with the request/task context passed into emit(ctx). Since emit already has a context, consider threading it through to these helpers (e.g., add a ctx parameter) so transformer regions appear under the correct task/region in traces.
| @@ -214,11 +216,15 @@ func createCheckerPool(program checker.Program) (getChecker func() (*checker.Che | |||
| current := created.Load() | |||
| if current >= maxSize { | |||
| // At limit, wait for one to become available | |||
| ch := <-pool | |||
| return ch, func() { pool <- ch } | |||
| select { | |||
| case ch := <-pool: | |||
| return ch, func() { pool <- ch } | |||
| case <-ctx.Done(): | |||
| return nil, func() {} | |||
| } | |||
| } | |||
| if created.CompareAndSwap(current, current+1) { | |||
| ch := core.FirstResult(checker.NewChecker(program, nil)) | |||
| ch := core.FirstResult(checker.NewChecker(ctx, program, nil)) | |||
| return ch, func() { pool <- ch } | |||
| } | |||
| } | |||
There was a problem hiding this comment.
createCheckerPool returns early on <-ctx.Done() in a couple of places, but if the context is canceled after the initial non-blocking select and before/while the CAS loop runs, it can still spin and/or create a new checker even though cancellation was requested. Consider checking ctx.Err() inside the for { ... } loop (and before creating a new checker) so cancellation reliably stops work.
This adds Go
runtime/tracesupport, including flight recording. Traces are integrated into the VS Code extension, similarly to pprof profiling.I've added trace points in various places, and plumbed context around a bit.