From cc29f499151e0f581119cfaa7d38dfbfd83beb25 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 23 Oct 2026 13:09:39 -0400 Subject: [PATCH 1/4] perf(aws-lambda): warm the audio function while the plan runs The audio task runs on the x86 render function, which is cold when the plan ran on the arm64 video function. A fire-and-forget invoke at the start of a render now wakes it, so the audio no longer outlasts the chunks. --- examples/aws-lambda/template.yaml | 21 +++++++++++++++++-- .../HyperframesRenderStack.snapshot.test.ts | 19 ++++++++++++++--- .../src/cdk/HyperframesRenderStack.ts | 14 +++++++++++-- packages/aws-lambda/src/events.ts | 10 +++++++++ packages/aws-lambda/src/handler.test.ts | 13 ++++++++++++ packages/aws-lambda/src/handler.ts | 5 +++++ 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/examples/aws-lambda/template.yaml b/examples/aws-lambda/template.yaml index 7f098086171..5170edaa69f 100644 --- a/examples/aws-lambda/template.yaml +++ b/examples/aws-lambda/template.yaml @@ -256,14 +256,31 @@ Resources: Choices: - Variable: $.PlanProtocol StringEquals: v2 - Next: PlanV2 + Next: WarmRender - Variable: $.PlanProtocol StringEquals: v1 Next: Plan - Variable: $.PlanProtocol IsPresent: true Next: UnsupportedPlanProtocol - Default: PlanV2 + Default: WarmRender + + # The audio task runs on this function once the plan is done; starting its container now hides the cold start. + WarmRender: + Type: Task + Resource: arn:aws:states:::lambda:invoke + Parameters: + FunctionName: !GetAtt RenderFunction.Arn + InvocationType: Event + Payload: + Action: warm + ResultPath: null + Catch: + - ErrorEquals: + - States.ALL + ResultPath: null + Next: PlanV2 + Next: PlanV2 UnsupportedPlanProtocol: Type: Fail diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts index 2977fa98e34..21b6ab19935 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts @@ -58,6 +58,7 @@ const EXPECTED_RESOURCE_COUNTS: Record = { // not at this level — we cover it separately in the contract test. const EXPECTED_STATE_NAMES = [ "SelectPlanProtocol", + "WarmRender", "Plan", "PlanV2", "BuildChunkList", @@ -161,13 +162,23 @@ describe("HyperframesRenderStack — snapshot", () => { expect(actualStates.sort()).toEqual([...EXPECTED_STATE_NAMES].sort()); }); + it("warms the render function before the plan, and a failed warm-up never fails the render", () => { + for (const definition of [SYNTHED.definition, readSamDefinition()]) { + const warm = requireRecord(definition.States.WarmRender, "WarmRender state"); + expect(sentPayload(warm)).toEqual({ Action: "warm" }); + expect(requireRecordProperty(warm, "Parameters", "parameters").InvocationType).toBe("Event"); + expect(warm.Next).toBe("PlanV2"); + expect(JSON.stringify(warm.Catch)).toContain('"Next":"PlanV2"'); + } + }); + it("defaults omitted plan protocol to v2 and preserves the explicit v1 branch", () => { for (const definition of [SYNTHED.definition, readSamDefinition()]) { const selection = requireRecord( definition.States.SelectPlanProtocol, "SelectPlanProtocol state", ); - expect(selection.Default).toBe("PlanV2"); + expect(selection.Default).toBe("WarmRender"); expect(JSON.stringify(selection)).toContain('"StringEquals":"v1"'); expect(JSON.stringify(definition.States.Plan)).toContain('"PlanProtocol":"v1"'); } @@ -346,14 +357,14 @@ describe("HyperframesRenderStack — snapshot", () => { }); it("runs plan and renderChunk on the SAM template's optional arm64 video function", () => { - // A chunk refuses a plan made with another ffmpeg build; audio and assemble stay on x86. + // A chunk refuses a plan made with another ffmpeg build; warm-up, audio and assemble stay on x86. const targets = Object.values(lambdaTasks(readSamDefinition().States)).map((task) => ({ action: sentPayload(task).Action, functionName: requireRecordProperty(requireRecord(task, "task"), "Parameters", "task params") .FunctionName, })); expect(new Set(targets.map((target) => target.action))).toEqual( - new Set(["plan", "renderChunk", "audio", "assemble"]), + new Set(["warm", "plan", "renderChunk", "audio", "assemble"]), ); for (const { action, functionName } of targets) { expect({ action, functionName }).toEqual({ @@ -491,6 +502,8 @@ describe("HyperframesRenderStack — snapshot", () => { it("gives Lambda throttles a minutes-long backoff ahead of the catch-all, in CDK and SAM", () => { for (const definition of [SYNTHED.definition, readSamDefinition()]) { for (const [taskName, task] of Object.entries(lambdaTasks(definition.States))) { + // The warm-up is fire-and-forget: a throttle is caught and the render goes on without it. + if (taskName === "WarmRender") continue; expect({ taskName, ...throttleRetry(task) }).toEqual({ taskName, beforeCatchAll: true, diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts index 864ab61b433..d379be829ec 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts @@ -332,6 +332,16 @@ export class HyperframesRenderStack extends Construct { maxDelay: Duration.seconds(60), }); + // The audio task runs on this function once the plan is done; starting its container now hides the cold start. + const warmRender = new tasks.LambdaInvoke(this, "WarmRender", { + lambdaFunction: this.renderFunction, + invocationType: tasks.LambdaInvocationType.EVENT, + payload: sfn.TaskInput.fromObject({ Action: "warm" }), + resultPath: sfn.JsonPath.DISCARD, + }); + warmRender.addCatch(planV2, { errors: ["States.ALL"], resultPath: sfn.JsonPath.DISCARD }); + warmRender.next(planV2); + const buildChunkList = new sfn.Pass(this, "BuildChunkList", { parameters: { "ChunkIndexes.$": "States.ArrayRange(0, States.MathAdd($.Plan.ChunkCount, -1), 1)", @@ -557,10 +567,10 @@ export class HyperframesRenderStack extends Construct { cause: 'PlanProtocol must be "v1", "v2", or absent (defaults to v2).', }); return new sfn.Choice(this, "SelectPlanProtocol") - .when(sfn.Condition.stringEquals("$.PlanProtocol", "v2"), planV2) + .when(sfn.Condition.stringEquals("$.PlanProtocol", "v2"), warmRender) .when(sfn.Condition.stringEquals("$.PlanProtocol", "v1"), plan) .when(sfn.Condition.isPresent("$.PlanProtocol"), unsupportedPlanProtocol) - .otherwise(planV2); + .otherwise(warmRender); } } diff --git a/packages/aws-lambda/src/events.ts b/packages/aws-lambda/src/events.ts index 384cf7a0c5b..5ba210e4ebb 100644 --- a/packages/aws-lambda/src/events.ts +++ b/packages/aws-lambda/src/events.ts @@ -25,6 +25,10 @@ export type { SerializableDistributedRenderConfig } from "@hyperframes/producer/ /** Discriminator for the three roles the one Lambda image fulfills. */ export type LambdaAction = "plan" | "audio" | "renderChunk" | "assemble"; +/** Sent by the state machine, not unwrapped: makes a cold function ready for the `audio` task that follows. */ +export interface WarmEvent { + Action: "warm"; +} /** Transport protocol selected for one complete distributed render. */ export type LambdaPlanProtocol = "v1" | "v2"; @@ -41,6 +45,7 @@ export type LambdaEvent = | AudioEvent | RenderChunkEvent | AssembleEvent + | WarmEvent | { Payload: LambdaEvent } | { Input: LambdaEvent }; @@ -231,7 +236,12 @@ export interface AssembleLambdaResult { DurationMs: number; } +export interface WarmLambdaResult { + Action: "warm"; +} + export type LambdaResult = + | WarmLambdaResult | PlanLambdaResult | AudioLambdaResult | RenderChunkLambdaResult diff --git a/packages/aws-lambda/src/handler.test.ts b/packages/aws-lambda/src/handler.test.ts index 8f0d2a1d4ab..77333dd44b3 100644 --- a/packages/aws-lambda/src/handler.test.ts +++ b/packages/aws-lambda/src/handler.test.ts @@ -255,6 +255,19 @@ describe("unwrapEvent", () => { }); describe("handler dispatch", () => { + it("answers a warm event without touching S3 or the project", async () => { + const s3 = new FakeS3Client(); + expect( + await handleEvent( + { Action: "warm" }, + { s3: s3 as unknown as import("@aws-sdk/client-s3").S3Client, skipChromeResolution: true }, + ), + ).toEqual({ + Action: "warm", + }); + expect(s3.ops).toEqual([]); + }); + it("preserves explicit v1 plan compatibility", async () => { const tmpRoot = makeTmpRoot(); const s3 = new FakeS3Client(); diff --git a/packages/aws-lambda/src/handler.ts b/packages/aws-lambda/src/handler.ts index 8bab85b1a81..d7d0e56fc47 100644 --- a/packages/aws-lambda/src/handler.ts +++ b/packages/aws-lambda/src/handler.ts @@ -121,6 +121,11 @@ export interface HandlerDeps { * shape; unwrap until we hit a discriminated event. */ export async function handler(event: LambdaEvent, deps?: HandlerDeps): Promise { + if ((event as { Action?: unknown }).Action === "warm") { + primeRuntimeEnv(); + await ensureChromePath(deps); + return { Action: "warm" }; + } const unwrapped = unwrapEvent(event); validatePlanProtocolShape(unwrapped); validateEventS3Uris(unwrapped); From c685a8c6eb5ee20c29b34c8a0917a76c91d6dd28 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 23 Oct 2026 13:35:02 -0400 Subject: [PATCH 2/4] perf(aws-lambda): warm the render function only on a stack with a video function An x86-only stack has no cold function to warm, so it takes the old path with no extra step. The CDK stack has no video function and is unchanged. --- examples/aws-lambda/template.yaml | 8 +++-- .../HyperframesRenderStack.snapshot.test.ts | 32 ++++++++++++------- .../src/cdk/HyperframesRenderStack.ts | 14 ++------ packages/aws-lambda/src/events.ts | 1 - 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/examples/aws-lambda/template.yaml b/examples/aws-lambda/template.yaml index 5170edaa69f..bba31313e42 100644 --- a/examples/aws-lambda/template.yaml +++ b/examples/aws-lambda/template.yaml @@ -254,16 +254,20 @@ Resources: SelectPlanProtocol: Type: Choice Choices: + # Only a stack with the arm64 video function has a cold x86 function to warm; without one this never matches. - Variable: $.PlanProtocol - StringEquals: v2 + StringEquals: !If [HasVideoFunction, v2, v2-without-video-function] Next: WarmRender + - Variable: $.PlanProtocol + StringEquals: v2 + Next: PlanV2 - Variable: $.PlanProtocol StringEquals: v1 Next: Plan - Variable: $.PlanProtocol IsPresent: true Next: UnsupportedPlanProtocol - Default: WarmRender + Default: PlanV2 # The audio task runs on this function once the plan is done; starting its container now hides the cold start. WarmRender: diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts index 21b6ab19935..67216e978cc 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts @@ -58,7 +58,6 @@ const EXPECTED_RESOURCE_COUNTS: Record = { // not at this level — we cover it separately in the contract test. const EXPECTED_STATE_NAMES = [ "SelectPlanProtocol", - "WarmRender", "Plan", "PlanV2", "BuildChunkList", @@ -162,14 +161,24 @@ describe("HyperframesRenderStack — snapshot", () => { expect(actualStates.sort()).toEqual([...EXPECTED_STATE_NAMES].sort()); }); - it("warms the render function before the plan, and a failed warm-up never fails the render", () => { - for (const definition of [SYNTHED.definition, readSamDefinition()]) { - const warm = requireRecord(definition.States.WarmRender, "WarmRender state"); - expect(sentPayload(warm)).toEqual({ Action: "warm" }); - expect(requireRecordProperty(warm, "Parameters", "parameters").InvocationType).toBe("Event"); - expect(warm.Next).toBe("PlanV2"); - expect(JSON.stringify(warm.Catch)).toContain('"Next":"PlanV2"'); - } + it("warms the render function before a v2 plan only on a stack with a video function, and a failed warm-up is caught", () => { + const states = readSamDefinition().States; + const warm = requireRecord(states.WarmRender, "WarmRender state"); + expect(sentPayload(warm)).toEqual({ Action: "warm" }); + expect(requireRecordProperty(warm, "Parameters", "parameters").InvocationType).toBe("Event"); + expect(warm.Next).toBe("PlanV2"); + expect(JSON.stringify(warm.Catch)).toContain('"Next":"PlanV2"'); + const choices = requireRecord(states.SelectPlanProtocol, "SelectPlanProtocol").Choices as Record< + string, + unknown + >[]; + const toWarm = choices.filter((choice) => choice.Next === "WarmRender"); + expect(toWarm).toHaveLength(1); + expect(toWarm[0]).toEqual({ + Variable: "$.PlanProtocol", + StringEquals: ["HasVideoFunction", "v2", "v2-without-video-function"], + Next: "WarmRender", + }); }); it("defaults omitted plan protocol to v2 and preserves the explicit v1 branch", () => { @@ -178,7 +187,7 @@ describe("HyperframesRenderStack — snapshot", () => { definition.States.SelectPlanProtocol, "SelectPlanProtocol state", ); - expect(selection.Default).toBe("WarmRender"); + expect(selection.Default).toBe("PlanV2"); expect(JSON.stringify(selection)).toContain('"StringEquals":"v1"'); expect(JSON.stringify(definition.States.Plan)).toContain('"PlanProtocol":"v1"'); } @@ -345,7 +354,7 @@ describe("HyperframesRenderStack — snapshot", () => { // CDK drops null-valued payload fields when it renders the definition (#5259), so compare // what each deployed task sends rather than the construct source. const cdkTasks = lambdaTasks(SYNTHED.definition.States); - const samTasks = lambdaTasks(readSamDefinition().States); + const { WarmRender: _warm, ...samTasks } = lambdaTasks(readSamDefinition().States); expect(Object.keys(cdkTasks).sort()).toEqual(Object.keys(samTasks).sort()); expect(Object.keys(cdkTasks).length).toBeGreaterThanOrEqual(6); for (const [taskName, cdkTask] of Object.entries(cdkTasks)) { @@ -502,7 +511,6 @@ describe("HyperframesRenderStack — snapshot", () => { it("gives Lambda throttles a minutes-long backoff ahead of the catch-all, in CDK and SAM", () => { for (const definition of [SYNTHED.definition, readSamDefinition()]) { for (const [taskName, task] of Object.entries(lambdaTasks(definition.States))) { - // The warm-up is fire-and-forget: a throttle is caught and the render goes on without it. if (taskName === "WarmRender") continue; expect({ taskName, ...throttleRetry(task) }).toEqual({ taskName, diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts index d379be829ec..864ab61b433 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts @@ -332,16 +332,6 @@ export class HyperframesRenderStack extends Construct { maxDelay: Duration.seconds(60), }); - // The audio task runs on this function once the plan is done; starting its container now hides the cold start. - const warmRender = new tasks.LambdaInvoke(this, "WarmRender", { - lambdaFunction: this.renderFunction, - invocationType: tasks.LambdaInvocationType.EVENT, - payload: sfn.TaskInput.fromObject({ Action: "warm" }), - resultPath: sfn.JsonPath.DISCARD, - }); - warmRender.addCatch(planV2, { errors: ["States.ALL"], resultPath: sfn.JsonPath.DISCARD }); - warmRender.next(planV2); - const buildChunkList = new sfn.Pass(this, "BuildChunkList", { parameters: { "ChunkIndexes.$": "States.ArrayRange(0, States.MathAdd($.Plan.ChunkCount, -1), 1)", @@ -567,10 +557,10 @@ export class HyperframesRenderStack extends Construct { cause: 'PlanProtocol must be "v1", "v2", or absent (defaults to v2).', }); return new sfn.Choice(this, "SelectPlanProtocol") - .when(sfn.Condition.stringEquals("$.PlanProtocol", "v2"), warmRender) + .when(sfn.Condition.stringEquals("$.PlanProtocol", "v2"), planV2) .when(sfn.Condition.stringEquals("$.PlanProtocol", "v1"), plan) .when(sfn.Condition.isPresent("$.PlanProtocol"), unsupportedPlanProtocol) - .otherwise(warmRender); + .otherwise(planV2); } } diff --git a/packages/aws-lambda/src/events.ts b/packages/aws-lambda/src/events.ts index 5ba210e4ebb..7fe1b2e0f16 100644 --- a/packages/aws-lambda/src/events.ts +++ b/packages/aws-lambda/src/events.ts @@ -25,7 +25,6 @@ export type { SerializableDistributedRenderConfig } from "@hyperframes/producer/ /** Discriminator for the three roles the one Lambda image fulfills. */ export type LambdaAction = "plan" | "audio" | "renderChunk" | "assemble"; -/** Sent by the state machine, not unwrapped: makes a cold function ready for the `audio` task that follows. */ export interface WarmEvent { Action: "warm"; } From a25f2acf1d3edf0057c086c1b5b02d7b5017efa7 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 23 Oct 2026 13:37:25 -0400 Subject: [PATCH 3/4] perf(aws-lambda): wrap a template comment --- examples/aws-lambda/template.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/aws-lambda/template.yaml b/examples/aws-lambda/template.yaml index bba31313e42..604138b020a 100644 --- a/examples/aws-lambda/template.yaml +++ b/examples/aws-lambda/template.yaml @@ -254,7 +254,8 @@ Resources: SelectPlanProtocol: Type: Choice Choices: - # Only a stack with the arm64 video function has a cold x86 function to warm; without one this never matches. + # Only a stack with the arm64 video function has a cold x86 function to warm. + # Without one the compared value can never match. - Variable: $.PlanProtocol StringEquals: !If [HasVideoFunction, v2, v2-without-video-function] Next: WarmRender From b4096408ec6d340dc9049dac7c344d00a191918e Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 23 Oct 2026 13:56:59 -0400 Subject: [PATCH 4/4] style(aws-lambda): format the warm-up template test --- .../src/cdk/HyperframesRenderStack.snapshot.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts index 67216e978cc..71198f23303 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts @@ -168,10 +168,8 @@ describe("HyperframesRenderStack — snapshot", () => { expect(requireRecordProperty(warm, "Parameters", "parameters").InvocationType).toBe("Event"); expect(warm.Next).toBe("PlanV2"); expect(JSON.stringify(warm.Catch)).toContain('"Next":"PlanV2"'); - const choices = requireRecord(states.SelectPlanProtocol, "SelectPlanProtocol").Choices as Record< - string, - unknown - >[]; + const choices = requireRecord(states.SelectPlanProtocol, "SelectPlanProtocol") + .Choices as Record[]; const toWarm = choices.filter((choice) => choice.Next === "WarmRender"); expect(toWarm).toHaveLength(1); expect(toWarm[0]).toEqual({