Skip to content

Commit c366206

Browse files
kalyazinclaude
andcommitted
feat(orch): collapse envd heap before pause behind a feature flag
Mirror the user-cgroup-freeze pre-pause path: when the new collapse-envd-heap LaunchDarkly flag is set, the orchestrator calls envd's POST /collapse just before pause so envd consolidates its scattered heap into hugepages and faults fewer distinct frames on resume. Gate on envd version (>= 0.6.4, which exposes the endpoint) with a dedicated 10s timeout, and keep it best-effort so a failure never blocks pause. The flag defaults off and is rolled out via LaunchDarkly. Observe the collapse on a dedicated envd-collapse span and two metrics: the orchestrator.sandbox.envd.collapse.duration histogram (round-trip ms, with a success attribute) for the pause-path cost, and the orchestrator.sandbox.envd.collapse.chunks counter split by result (collapsed|skipped) so chunk attempts (total) and successes (collapsed) are both queryable. The per-call stats are attached as span attributes and logged on success ("envd heap collapsed") so efficacy is visible in Loki when the metric counters reset on restart. Add a resume-build -collapse flag that overrides the LD flag on for local testing. Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3bdce67 commit c366206

8 files changed

Lines changed: 183 additions & 7 deletions

File tree

‎packages/orchestrator/cmd/resume-build/main.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func main() {
7575

7676
fphTimeoutMs := flag.Int("fph-timeout-ms", 0, "override free-page-hinting-config pause timeout LD flag (0 = use LD default)")
7777
reclaim := flag.Bool("reclaim", false, "enable pre-pause reclaim chain (fstrim 500ms, sync 500ms, drop_caches 200ms, compact 1s)")
78+
collapse := flag.Bool("collapse", false, "collapse envd's heap before pause (overrides the collapse-envd-heap flag)")
7879

7980
fphBench := flag.Bool("fph-bench", false, "compare pause memfile size with vs without FPH; requires -cmd-pause workload, uses -iterations (default 3), forces FPR on")
8081
fphBenchDelay := flag.Duration("fph-bench-delay", 0, "wait this long between workload completion and pause (lets FPR settle)")
@@ -101,6 +102,10 @@ func main() {
101102
featureflags.OverrideBoolFlag(featureflags.UseMemFdFlag, true)
102103
}
103104

105+
if *collapse {
106+
featureflags.OverrideBoolFlag(featureflags.CollapseEnvdHeapFlag, true)
107+
}
108+
104109
if *memfileDiffDedup {
105110
featureflags.OverrideJSONFlag(featureflags.MemfileDiffDedupFlag, ldvalue.FromJSONMarshal(map[string]any{
106111
"enabled": true,

‎packages/orchestrator/pkg/sandbox/envd.go‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,72 @@ func (s *Sandbox) callEnvdUnfreeze(ctx context.Context, timeout time.Duration) e
108108
}
109109

110110
func (s *Sandbox) callEnvdCgroupOp(ctx context.Context, timeout time.Duration, op envdCgroupOp) error {
111+
return s.postEnvd(ctx, timeout, string(op))
112+
}
113+
114+
// callEnvdCollapse calls envd's native POST /collapse endpoint, which compacts
115+
// envd's own anonymous heap into 2 MiB hugepages before pause so it faults
116+
// fewer distinct frames on resume. Unlike freeze/unfreeze it returns a body:
117+
// the per-call collapse stats, which the caller records as metrics and span
118+
// attributes.
119+
func (s *Sandbox) callEnvdCollapse(ctx context.Context, timeout time.Duration) (envd.CollapseResult, error) {
120+
ctx, cancel := context.WithTimeout(ctx, timeout)
121+
defer cancel()
122+
123+
address := fmt.Sprintf("http://%s:%d/collapse", s.Slot.HostIPString(), consts.DefaultEnvdServerPort)
124+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, address, nil)
125+
if err != nil {
126+
return envd.CollapseResult{}, fmt.Errorf("build collapse request: %w", err)
127+
}
128+
if s.Config.Envd.AccessToken != nil {
129+
req.Header.Set("X-Access-Token", *s.Config.Envd.AccessToken)
130+
}
131+
132+
resp, err := sandboxHttpClient.Do(req)
133+
if err != nil {
134+
return envd.CollapseResult{}, fmt.Errorf("collapse request: %w", err)
135+
}
136+
defer resp.Body.Close()
137+
138+
if resp.StatusCode != http.StatusOK {
139+
body, _ := io.ReadAll(resp.Body)
140+
141+
return envd.CollapseResult{}, fmt.Errorf("collapse returned %d: %s", resp.StatusCode, utils.Truncate(string(body), 100))
142+
}
143+
144+
var result envd.CollapseResult
145+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
146+
return envd.CollapseResult{}, fmt.Errorf("decode collapse result: %w", err)
147+
}
148+
149+
return result, nil
150+
}
151+
152+
// postEnvd issues an authenticated POST to envd's /<path> endpoint with a tight,
153+
// dedicated deadline and expects 204 No Content.
154+
func (s *Sandbox) postEnvd(ctx context.Context, timeout time.Duration, path string) error {
111155
ctx, cancel := context.WithTimeout(ctx, timeout)
112156
defer cancel()
113157

114-
address := fmt.Sprintf("http://%s:%d/%s", s.Slot.HostIPString(), consts.DefaultEnvdServerPort, op)
158+
address := fmt.Sprintf("http://%s:%d/%s", s.Slot.HostIPString(), consts.DefaultEnvdServerPort, path)
115159
req, err := http.NewRequestWithContext(ctx, http.MethodPost, address, nil)
116160
if err != nil {
117-
return fmt.Errorf("build %s request: %w", op, err)
161+
return fmt.Errorf("build %s request: %w", path, err)
118162
}
119163
if s.Config.Envd.AccessToken != nil {
120164
req.Header.Set("X-Access-Token", *s.Config.Envd.AccessToken)
121165
}
122166

123167
resp, err := sandboxHttpClient.Do(req)
124168
if err != nil {
125-
return fmt.Errorf("%s request: %w", op, err)
169+
return fmt.Errorf("%s request: %w", path, err)
126170
}
127171
defer resp.Body.Close()
128172

129173
if resp.StatusCode != http.StatusNoContent {
130174
body, _ := io.ReadAll(resp.Body)
131175

132-
return fmt.Errorf("%s returned %d: %s", op, resp.StatusCode, utils.Truncate(string(body), 100))
176+
return fmt.Errorf("%s returned %d: %s", path, resp.StatusCode, utils.Truncate(string(body), 100))
133177
}
134178

135179
return nil

‎packages/orchestrator/pkg/sandbox/envd/envd.gen.go‎

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/orchestrator/pkg/sandbox/reclaim.go‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"strings"
99
"time"
1010

11+
"go.opentelemetry.io/otel/attribute"
12+
"go.opentelemetry.io/otel/codes"
13+
"go.opentelemetry.io/otel/metric"
1114
"go.uber.org/zap"
1215

1316
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
@@ -23,6 +26,12 @@ const reclaimOuterSlack = 500 * time.Millisecond
2326
// must stay independent of the reclaim shell deadline.
2427
const freezeTimeout = 2 * time.Second
2528

29+
// collapseTimeout bounds the native POST /collapse call. Collapsing migrates
30+
// envd's scattered heap pages into hugepages, which is heavier than the freeze
31+
// sysfs write, so it gets a larger, independent budget. Best-effort: if it is
32+
// cut short, the partially-collapsed heap is still an improvement.
33+
const collapseTimeout = 10 * time.Second
34+
2635
// buildReclaimScript builds the fstrim/sync/drop_caches/compact_memory chain.
2736
// Returns ("", 0) when every step is disabled.
2837
func (s *Sandbox) buildReclaimScript(cfg featureflags.ReclaimConfig) (string, time.Duration) {
@@ -76,6 +85,14 @@ func (s *Sandbox) bestEffortReclaim(ctx context.Context) {
7685
s.bestEffortFreeze(ctx)
7786
}
7887

88+
if s.featureFlags.BoolFlag(ctx, featureflags.CollapseEnvdHeapFlag,
89+
featureflags.SandboxContext(s.Runtime.SandboxID),
90+
featureflags.TeamContext(s.Runtime.TeamID),
91+
featureflags.TemplateContext(s.Runtime.TemplateID),
92+
) {
93+
s.bestEffortCollapse(ctx)
94+
}
95+
7996
script, timeout := s.buildReclaimScript(cfg)
8097
if script == "" {
8198
return
@@ -122,6 +139,73 @@ func (s *Sandbox) envdSupportsCgroupFreeze(ctx context.Context) bool {
122139
return ok
123140
}
124141

142+
// envdSupportsHeapCollapse reports whether the sandbox's envd exposes the native
143+
// /collapse endpoint. Bad version strings log and return false so we never call
144+
// an unsupported endpoint.
145+
func (s *Sandbox) envdSupportsHeapCollapse(ctx context.Context) bool {
146+
ok, err := utils.IsGTEVersion(s.Config.Envd.Version, utils.MinEnvdVersionForHeapCollapse)
147+
if err != nil {
148+
logger.L().Warn(ctx, "heap collapse version gate: bad envd version", logger.WithSandboxID(s.Runtime.SandboxID), zap.String("envd_version", s.Config.Envd.Version), zap.Error(err))
149+
150+
return false
151+
}
152+
153+
return ok
154+
}
155+
156+
// bestEffortCollapse asks envd to collapse its own heap into hugepages before
157+
// pause, so on resume envd touches fewer distinct frames. Gated on envd version;
158+
// failures are logged but never block pause.
159+
func (s *Sandbox) bestEffortCollapse(ctx context.Context) {
160+
if !s.envdSupportsHeapCollapse(ctx) {
161+
return
162+
}
163+
164+
ctx, span := tracer.Start(ctx, "envd-collapse")
165+
defer span.End()
166+
167+
start := time.Now()
168+
stats, err := s.callEnvdCollapse(ctx, collapseTimeout)
169+
elapsedMs := time.Since(start).Milliseconds()
170+
success := err == nil
171+
172+
// Record the round-trip duration whether or not it succeeded: a timed-out or
173+
// failed collapse still spends time on the pause path and must be visible.
174+
envdCollapseDurationHistogram.Record(ctx, elapsedMs, metric.WithAttributes(attribute.Bool("success", success)))
175+
span.SetAttributes(
176+
attribute.Bool("collapse.success", success),
177+
attribute.Int64("collapse.duration_ms", elapsedMs),
178+
)
179+
180+
if err != nil {
181+
span.SetStatus(codes.Error, err.Error())
182+
logger.L().Warn(ctx, "envd heap collapse failed", logger.WithSandboxID(s.Runtime.SandboxID), zap.Error(err))
183+
184+
return
185+
}
186+
187+
// Chunk-level efficacy: attempts = collapsed + skipped, successful = collapsed.
188+
// One counter split by result so attempts (sum) and successes (collapsed) are
189+
// both queryable.
190+
envdCollapseChunks.Add(ctx, int64(stats.Collapsed), metric.WithAttributes(attribute.String("result", "collapsed")))
191+
envdCollapseChunks.Add(ctx, int64(stats.Skipped), metric.WithAttributes(attribute.String("result", "skipped")))
192+
span.SetAttributes(
193+
attribute.Int("collapse.regions", stats.Regions),
194+
attribute.Int("collapse.chunks", stats.Chunks),
195+
attribute.Int("collapse.collapsed", stats.Collapsed),
196+
attribute.Int("collapse.skipped", stats.Skipped),
197+
)
198+
199+
logger.L().Info(ctx, "envd heap collapsed",
200+
logger.WithSandboxID(s.Runtime.SandboxID),
201+
zap.Int("regions", stats.Regions),
202+
zap.Int("chunks", stats.Chunks),
203+
zap.Int("collapsed", stats.Collapsed),
204+
zap.Int("skipped", stats.Skipped),
205+
zap.Int64("duration_ms", elapsedMs),
206+
)
207+
}
208+
125209
// bestEffortFreeze calls envd's native /freeze endpoint with a tight, freeze-
126210
// only deadline so it doesn't share a timeout budget with the rest of reclaim.
127211
// Gated on envd version; failures are logged but never block pause.

‎packages/orchestrator/pkg/sandbox/sandbox.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ import (
4545
)
4646

4747
var (
48-
meter = otel.Meter("github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox")
49-
envdInitCalls = utils.Must(telemetry.GetCounter(meter, telemetry.EnvdInitCalls))
50-
waitForEnvdDurationHistogram = utils.Must(telemetry.GetHistogram(meter, telemetry.WaitForEnvdDurationHistogramName))
48+
meter = otel.Meter("github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox")
49+
envdInitCalls = utils.Must(telemetry.GetCounter(meter, telemetry.EnvdInitCalls))
50+
waitForEnvdDurationHistogram = utils.Must(telemetry.GetHistogram(meter, telemetry.WaitForEnvdDurationHistogramName))
51+
envdCollapseDurationHistogram = utils.Must(telemetry.GetHistogram(meter, telemetry.EnvdCollapseDurationHistogramName))
52+
envdCollapseChunks = utils.Must(telemetry.GetCounter(meter, telemetry.EnvdCollapseChunks))
5153

5254
uffdStartupPagesHistogram = utils.Must(telemetry.GetHistogram(meter, telemetry.UffdStartupPagesHistogramName))
5355
uffdStartupSourcePagesHistogram = utils.Must(telemetry.GetHistogram(meter, telemetry.UffdStartupSourcePagesHistogramName))

‎packages/shared/pkg/featureflags/flags.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ var (
160160
OptimisticResourceAccountingFlag = NewBoolFlag("sandbox-placement-optimistic-resource-accounting", false)
161161
FreePageReportingFlag = NewBoolFlag("free-page-reporting", false)
162162
FreezeUserCgroupFlag = NewBoolFlag("freeze-user-cgroup", env.IsDevelopment())
163+
// CollapseEnvdHeapFlag makes the orchestrator ask envd to collapse its own
164+
// anonymous heap into 2 MiB hugepages just before pause, reducing the number
165+
// of distinct frames envd faults on resume. Off by default; rolled out via LD.
166+
CollapseEnvdHeapFlag = NewBoolFlag("collapse-envd-heap", false)
163167

164168
NetworkTransformRulesFlag = NewBoolFlag("network-transform-rules", env.IsDevelopment())
165169

‎packages/shared/pkg/telemetry/meters.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ const (
2626
TeamSandboxCreated CounterType = "e2b.team.sandbox.created"
2727

2828
EnvdInitCalls CounterType = "orchestrator.sandbox.envd.init.calls"
29+
30+
// 2 MiB chunks the pre-pause envd heap collapse attempted, split by the
31+
// result attribute (collapsed|skipped): attempts = total, successful =
32+
// collapsed.
33+
EnvdCollapseChunks CounterType = "orchestrator.sandbox.envd.collapse.chunks"
2934
// Incremented by the balance_dirty_pages thread count at every 200 ms poll
3035
// for the lifetime of the process. rate() shows dirty-page throttle
3136
// intensity in real-time; 0 when no stalls are occurring.
@@ -75,6 +80,11 @@ const (
7580
OrchestratorSandboxCreateDurationName HistogramType = "orchestrator.sandbox.create.duration"
7681
WaitForEnvdDurationHistogramName HistogramType = "orchestrator.sandbox.envd.init.duration"
7782

83+
// Pre-pause envd heap collapse round-trip duration (the pause-path cost of
84+
// POST /collapse: network plus envd's madvise work), recorded once per pause
85+
// when the collapse-envd-heap flag is on.
86+
EnvdCollapseDurationHistogramName HistogramType = "orchestrator.sandbox.envd.collapse.duration"
87+
7888
// Sandbox startup working-set histograms: demand-fault pages/bytes a guest
7989
// needed to reach a successful envd init, recorded once per start. Sampled
8090
// per start (not per fault), so histogram_quantile yields per-sandbox
@@ -186,6 +196,7 @@ var counterDesc = map[CounterType]string{
186196
TeamSandboxCreated: "Counter of started sandboxes for the team in the interval",
187197
OrchestratorHostBalanceDirtyPagesThreads: "Cumulative stalled thread-polls during sandbox resume; rate() gives throttle intensity",
188198
EnvdInitCalls: "Number of envd initialization calls",
199+
EnvdCollapseChunks: "2 MiB chunks the pre-pause envd heap collapse attempted, by result",
189200
OrchestratorSandboxKilledCounterName: "Number of sandboxes killed, labeled by kill reason",
190201
TCPFirewallConnectionsTotal: "Total number of TCP firewall connections processed",
191202
TCPFirewallErrorsTotal: "Total number of TCP firewall errors",
@@ -213,6 +224,7 @@ var counterUnits = map[CounterType]string{
213224
TeamSandboxCreated: "{sandbox}",
214225
OrchestratorHostBalanceDirtyPagesThreads: "{thread}",
215226
EnvdInitCalls: "1",
227+
EnvdCollapseChunks: "{chunk}",
216228
OrchestratorSandboxKilledCounterName: "{sandbox}",
217229
TCPFirewallConnectionsTotal: "{connection}",
218230
TCPFirewallErrorsTotal: "{error}",
@@ -389,6 +401,7 @@ var histogramDesc = map[HistogramType]string{
389401
BuildRootfsSizeHistogramName: "Size of the built template rootfs in bytes",
390402
OrchestratorSandboxCreateDurationName: "Time taken to create a sandbox",
391403
WaitForEnvdDurationHistogramName: "Time taken for Envd to initialize successfully",
404+
EnvdCollapseDurationHistogramName: "Time taken for the pre-pause envd heap collapse round-trip",
392405

393406
UffdStartupPagesHistogramName: "Demand-fault pages a guest needed to reach a successful envd init, per start",
394407
UffdStartupSourcePagesHistogramName: "Subset of startup demand-fault pages pulled from the source (e.g. GCS), per start",
@@ -434,6 +447,7 @@ var histogramUnits = map[HistogramType]string{
434447
BuildRootfsSizeHistogramName: "{By}",
435448
OrchestratorSandboxCreateDurationName: "ms",
436449
WaitForEnvdDurationHistogramName: "ms",
450+
EnvdCollapseDurationHistogramName: "ms",
437451
UffdStartupPagesHistogramName: "{page}",
438452
UffdStartupSourcePagesHistogramName: "{page}",
439453
UffdStartupBytesHistogramName: "{By}",

‎packages/shared/pkg/utils/version.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const MinEnvdVersionForSnapshot = "0.5.0"
1414
// sandbox doesn't end up permanently frozen after resume on older envds.
1515
const MinEnvdVersionForCgroupFreeze = "0.6.0"
1616

17+
// MinEnvdVersionForHeapCollapse is the first envd that exposes a native
18+
// POST /collapse endpoint, which compacts envd's own anonymous heap into 2 MiB
19+
// hugepages before pause to reduce the frames it faults on resume.
20+
const MinEnvdVersionForHeapCollapse = "0.6.4"
21+
1722
func sanitizeVersion(version string) string {
1823
if len(version) > 0 && version[0] != 'v' {
1924
version = "v" + version

0 commit comments

Comments
 (0)