Skip to content

Commit fb2f7c1

Browse files
perf(aws-lambda): warm the audio function while the plan runs (#6148)
* 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. * 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. * perf(aws-lambda): wrap a template comment * style(aws-lambda): format the warm-up template test
1 parent 9fe0511 commit fb2f7c1

5 files changed

Lines changed: 71 additions & 3 deletions

File tree

‎examples/aws-lambda/template.yaml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ Resources:
254254
SelectPlanProtocol:
255255
Type: Choice
256256
Choices:
257+
# Only a stack with the arm64 video function has a cold x86 function to warm.
258+
# Without one the compared value can never match.
259+
- Variable: $.PlanProtocol
260+
StringEquals: !If [HasVideoFunction, v2, v2-without-video-function]
261+
Next: WarmRender
257262
- Variable: $.PlanProtocol
258263
StringEquals: v2
259264
Next: PlanV2
@@ -265,6 +270,23 @@ Resources:
265270
Next: UnsupportedPlanProtocol
266271
Default: PlanV2
267272

273+
# The audio task runs on this function once the plan is done; starting its container now hides the cold start.
274+
WarmRender:
275+
Type: Task
276+
Resource: arn:aws:states:::lambda:invoke
277+
Parameters:
278+
FunctionName: !GetAtt RenderFunction.Arn
279+
InvocationType: Event
280+
Payload:
281+
Action: warm
282+
ResultPath: null
283+
Catch:
284+
- ErrorEquals:
285+
- States.ALL
286+
ResultPath: null
287+
Next: PlanV2
288+
Next: PlanV2
289+
268290
UnsupportedPlanProtocol:
269291
Type: Fail
270292
Error: PLAN_PROTOCOL_UNSUPPORTED

‎packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,24 @@ describe("HyperframesRenderStack — snapshot", () => {
161161
expect(actualStates.sort()).toEqual([...EXPECTED_STATE_NAMES].sort());
162162
});
163163

164+
it("warms the render function before a v2 plan only on a stack with a video function, and a failed warm-up is caught", () => {
165+
const states = readSamDefinition().States;
166+
const warm = requireRecord(states.WarmRender, "WarmRender state");
167+
expect(sentPayload(warm)).toEqual({ Action: "warm" });
168+
expect(requireRecordProperty(warm, "Parameters", "parameters").InvocationType).toBe("Event");
169+
expect(warm.Next).toBe("PlanV2");
170+
expect(JSON.stringify(warm.Catch)).toContain('"Next":"PlanV2"');
171+
const choices = requireRecord(states.SelectPlanProtocol, "SelectPlanProtocol")
172+
.Choices as Record<string, unknown>[];
173+
const toWarm = choices.filter((choice) => choice.Next === "WarmRender");
174+
expect(toWarm).toHaveLength(1);
175+
expect(toWarm[0]).toEqual({
176+
Variable: "$.PlanProtocol",
177+
StringEquals: ["HasVideoFunction", "v2", "v2-without-video-function"],
178+
Next: "WarmRender",
179+
});
180+
});
181+
164182
it("defaults omitted plan protocol to v2 and preserves the explicit v1 branch", () => {
165183
for (const definition of [SYNTHED.definition, readSamDefinition()]) {
166184
const selection = requireRecord(
@@ -334,7 +352,7 @@ describe("HyperframesRenderStack — snapshot", () => {
334352
// CDK drops null-valued payload fields when it renders the definition (#5259), so compare
335353
// what each deployed task sends rather than the construct source.
336354
const cdkTasks = lambdaTasks(SYNTHED.definition.States);
337-
const samTasks = lambdaTasks(readSamDefinition().States);
355+
const { WarmRender: _warm, ...samTasks } = lambdaTasks(readSamDefinition().States);
338356
expect(Object.keys(cdkTasks).sort()).toEqual(Object.keys(samTasks).sort());
339357
expect(Object.keys(cdkTasks).length).toBeGreaterThanOrEqual(6);
340358
for (const [taskName, cdkTask] of Object.entries(cdkTasks)) {
@@ -346,14 +364,14 @@ describe("HyperframesRenderStack — snapshot", () => {
346364
});
347365

348366
it("runs plan and renderChunk on the SAM template's optional arm64 video function", () => {
349-
// A chunk refuses a plan made with another ffmpeg build; audio and assemble stay on x86.
367+
// A chunk refuses a plan made with another ffmpeg build; warm-up, audio and assemble stay on x86.
350368
const targets = Object.values(lambdaTasks(readSamDefinition().States)).map((task) => ({
351369
action: sentPayload(task).Action,
352370
functionName: requireRecordProperty(requireRecord(task, "task"), "Parameters", "task params")
353371
.FunctionName,
354372
}));
355373
expect(new Set(targets.map((target) => target.action))).toEqual(
356-
new Set(["plan", "renderChunk", "audio", "assemble"]),
374+
new Set(["warm", "plan", "renderChunk", "audio", "assemble"]),
357375
);
358376
for (const { action, functionName } of targets) {
359377
expect({ action, functionName }).toEqual({
@@ -491,6 +509,7 @@ describe("HyperframesRenderStack — snapshot", () => {
491509
it("gives Lambda throttles a minutes-long backoff ahead of the catch-all, in CDK and SAM", () => {
492510
for (const definition of [SYNTHED.definition, readSamDefinition()]) {
493511
for (const [taskName, task] of Object.entries(lambdaTasks(definition.States))) {
512+
if (taskName === "WarmRender") continue;
494513
expect({ taskName, ...throttleRetry(task) }).toEqual({
495514
taskName,
496515
beforeCatchAll: true,

‎packages/aws-lambda/src/events.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export type { SerializableDistributedRenderConfig } from "@hyperframes/producer/
2525

2626
/** Discriminator for the three roles the one Lambda image fulfills. */
2727
export type LambdaAction = "plan" | "audio" | "renderChunk" | "assemble";
28+
export interface WarmEvent {
29+
Action: "warm";
30+
}
2831
/** Transport protocol selected for one complete distributed render. */
2932
export type LambdaPlanProtocol = "v1" | "v2";
3033

@@ -41,6 +44,7 @@ export type LambdaEvent =
4144
| AudioEvent
4245
| RenderChunkEvent
4346
| AssembleEvent
47+
| WarmEvent
4448
| { Payload: LambdaEvent }
4549
| { Input: LambdaEvent };
4650

@@ -231,7 +235,12 @@ export interface AssembleLambdaResult {
231235
DurationMs: number;
232236
}
233237

238+
export interface WarmLambdaResult {
239+
Action: "warm";
240+
}
241+
234242
export type LambdaResult =
243+
| WarmLambdaResult
235244
| PlanLambdaResult
236245
| AudioLambdaResult
237246
| RenderChunkLambdaResult

‎packages/aws-lambda/src/handler.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,19 @@ describe("unwrapEvent", () => {
255255
});
256256

257257
describe("handler dispatch", () => {
258+
it("answers a warm event without touching S3 or the project", async () => {
259+
const s3 = new FakeS3Client();
260+
expect(
261+
await handleEvent(
262+
{ Action: "warm" },
263+
{ s3: s3 as unknown as import("@aws-sdk/client-s3").S3Client, skipChromeResolution: true },
264+
),
265+
).toEqual({
266+
Action: "warm",
267+
});
268+
expect(s3.ops).toEqual([]);
269+
});
270+
258271
it("preserves explicit v1 plan compatibility", async () => {
259272
const tmpRoot = makeTmpRoot();
260273
const s3 = new FakeS3Client();

‎packages/aws-lambda/src/handler.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ export interface HandlerDeps {
121121
* shape; unwrap until we hit a discriminated event.
122122
*/
123123
export async function handler(event: LambdaEvent, deps?: HandlerDeps): Promise<LambdaResult> {
124+
if ((event as { Action?: unknown }).Action === "warm") {
125+
primeRuntimeEnv();
126+
await ensureChromePath(deps);
127+
return { Action: "warm" };
128+
}
124129
const unwrapped = unwrapEvent(event);
125130
validatePlanProtocolShape(unwrapped);
126131
validateEventS3Uris(unwrapped);

0 commit comments

Comments
 (0)