Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@remotion/lambda: Faster renders when using warm starts #2991

Merged
merged 10 commits into from
Oct 9, 2023
8 changes: 4 additions & 4 deletions packages/example/runlambda.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ cd lambda
npm run buildlambda
cd ..
cd example
pnpm exec remotion lambda functions rmall -f
pnpm exec remotion lambda functions deploy --memory=4000 --timeout=900 --disk=10000
pnpm exec remotion lambda sites create --site-name=testbed-v6 --log=verbose --enable-folder-expiry
# pnpm exec remotion lambda render testbed-v6 OffthreadRemoteVideo --log=verbose --delete-after="1-day"
bunx remotion lambda functions rmall -f
bunx remotion lambda functions deploy --memory=3000 --disk=10000
bunx remotion lambda sites create --site-name=testbed-v6 --log=verbose --enable-folder-expiry
bunx remotion lambda render testbed-v6 OffthreadRemoteVideo --log=verbose --delete-after="1-day"
2 changes: 1 addition & 1 deletion packages/example/src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export const Index: React.FC = () => {
width={1920}
height={1080}
fps={30}
durationInFrames={30 * 60 * 60}
durationInFrames={30 * 60}
/>
<Composition
id="video-testing-webm"
Expand Down
26 changes: 14 additions & 12 deletions packages/lambda/src/functions/compositions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,22 @@ export const compositionsHandler = async (

const region = getCurrentRegionInFunction();

const [bucketName, browserInstance] = await Promise.all([
lambdaParams.bucketName ??
internalGetOrCreateBucket({
const browserInstancePromise = getBrowserInstance(
lambdaParams.logLevel,
false,
lambdaParams.chromiumOptions,
);
const bucketNamePromise = lambdaParams.bucketName
? Promise.resolve(lambdaParams.bucketName)
: internalGetOrCreateBucket({
region,
enableFolderExpiry: null,
customCredentials: null,
}).then((b) => b.bucketName),
getBrowserInstance(
lambdaParams.logLevel,
false,
lambdaParams.chromiumOptions ?? {},
),
]);
}).then((b) => b.bucketName);

const bucketName = await bucketNamePromise;
const serializedInputPropsWithCustomSchema = await decompressInputProps({
bucketName,
bucketName: await bucketNamePromise,
expectedBucketOwner: options.expectedBucketOwner,
region: getCurrentRegionInFunction(),
serialized: lambdaParams.inputProps,
Expand All @@ -64,7 +64,7 @@ export const compositionsHandler = async (

const compositions = await RenderInternals.internalGetCompositions({
serveUrlOrWebpackUrl: realServeUrl,
puppeteerInstance: browserInstance,
puppeteerInstance: (await browserInstancePromise).instance,
serializedInputPropsWithCustomSchema,
envVariables: lambdaParams.envVariables ?? {},
timeoutInMilliseconds: lambdaParams.timeoutInMilliseconds,
Expand All @@ -78,6 +78,8 @@ export const compositionsHandler = async (
offthreadVideoCacheSizeInBytes: lambdaParams.offthreadVideoCacheSizeInBytes,
});

(await browserInstancePromise).instance.forgetEventLoop();

return Promise.resolve({
compositions,
type: 'success' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ let _browserInstance: Await<ReturnType<typeof openBrowser>> | null;

export const getBrowserInstance: typeof original = async () => {
_browserInstance = await openBrowser('chrome');
return _browserInstance;
return {instance: _browserInstance, configurationString: 'chrome'};
};
102 changes: 74 additions & 28 deletions packages/lambda/src/functions/helpers/get-browser-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@ import {RenderInternals} from '@remotion/renderer';
import type {Await} from '../../shared/await';
import {executablePath} from './get-chromium-executable-path';

let _browserInstance: Await<ReturnType<typeof openBrowser>> | null;
type LaunchedBrowser = {
instance: Await<ReturnType<typeof openBrowser>>;
configurationString: string;
};

const makeConfigurationString = (
options: ChromiumOptions,
logLevel: LogLevel,
): string => {
return [
`web-security-${Boolean(options.disableWebSecurity)}`,
`multi-process-${Boolean(options.enableMultiProcessOnLinux)}`,
`ignore-certificate-errors-${Boolean(options.ignoreCertificateErrors)}`,
`log-level-${logLevel}`,
`gl-${options.gl ?? null}`,
`userAgent-${options.userAgent ?? null}`,
`headless-${options.headless ?? false}`,
].join('/');
};

let _browserInstance: LaunchedBrowser | null;

let launching = false;

Expand All @@ -27,44 +47,70 @@ export const getBrowserInstance = async (
logLevel: LogLevel,
indent: boolean,
chromiumOptions: ChromiumOptions,
): ReturnType<typeof openBrowser> => {
): Promise<LaunchedBrowser> => {
const actualChromiumOptions: ChromiumOptions = {
...chromiumOptions,
// Override the `null` value, which might come from CLI with swANGLE
gl: chromiumOptions.gl ?? 'swangle',
};

if (launching) {
RenderInternals.Log.info('Already waiting for browser launch...');
await waitForLaunched();
if (!_browserInstance) {
throw new Error('expected to launch');
}

return _browserInstance;
}

if (_browserInstance) {
return _browserInstance;
}
const configurationString = makeConfigurationString(
actualChromiumOptions,
logLevel,
);

launching = true;
if (!_browserInstance) {
RenderInternals.Log.info(
'Cold Lambda function, launching new Lambda function',
);
launching = true;

const execPath = executablePath();
const execPath = executablePath();

const actualChromiumOptions: ChromiumOptions = {
...chromiumOptions,
// Override the `null` value, which might come from CLI with swANGLE
gl: chromiumOptions.gl ?? 'swangle',
};
const instance = await RenderInternals.internalOpenBrowser({
browser: 'chrome',
browserExecutable: execPath,
chromiumOptions: actualChromiumOptions,
forceDeviceScaleFactor: undefined,
indent: false,
viewport: null,
logLevel,
});
instance.on('disconnected', () => {
console.log('Browser disconnected / crashed');
_browserInstance?.instance
?.close(true, logLevel, indent)
.catch(() => undefined);
_browserInstance = null;
});
_browserInstance = {
instance,
configurationString,
};

_browserInstance = await RenderInternals.internalOpenBrowser({
browser: 'chrome',
browserExecutable: execPath,
chromiumOptions: actualChromiumOptions,
forceDeviceScaleFactor: undefined,
indent: false,
viewport: null,
logLevel,
});
_browserInstance.on('disconnected', () => {
console.log('Browser disconnected / crashed');
_browserInstance?.close(true, logLevel, indent).catch(() => undefined);
launching = false;
return _browserInstance;
}

if (_browserInstance.configurationString !== configurationString) {
RenderInternals.Log.info(
'Warm Lambda function, but Browser configuration changed. Killing old browser instance.',
);
_browserInstance.instance.rememberEventLoop();
await _browserInstance.instance.close(true, logLevel, indent);
_browserInstance = null;
});
launching = false;
return getBrowserInstance(logLevel, indent, chromiumOptions);
}

RenderInternals.Log.info('Warm Lambda function, reusing browser instance');
_browserInstance.instance.rememberEventLoop();
return _browserInstance;
};
6 changes: 0 additions & 6 deletions packages/lambda/src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,6 @@ const routine = async (

responseStream.write(JSON.stringify(res));
responseStream.end();
} finally {
responseStream.on('close', () => {
if (!process.env.VITEST) {
process.exit(0);
}
});
}
};

Expand Down
5 changes: 3 additions & 2 deletions packages/lambda/src/functions/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const innerLaunchHandler = async ({

const startedDate = Date.now();

const browserInstance = await getBrowserInstance(
const browserInstance = getBrowserInstance(
params.logLevel,
false,
params.chromiumOptions,
Expand All @@ -133,7 +133,7 @@ const innerLaunchHandler = async ({
const comp = await validateComposition({
serveUrl: params.serveUrl,
composition: params.composition,
browserInstance,
browserInstance: (await browserInstance).instance,
serializedInputPropsWithCustomSchema,
envVariables: params.envVariables ?? {},
timeoutInMilliseconds: params.timeoutInMilliseconds,
Expand Down Expand Up @@ -355,6 +355,7 @@ const innerLaunchHandler = async ({
);

reqSend.end();
(await browserInstance).instance.forgetEventLoop();

const fps = comp.fps / params.everyNthFrame;
const postRenderData = await mergeChunksAndFinishRender({
Expand Down
6 changes: 4 additions & 2 deletions packages/lambda/src/functions/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const renderHandler = async (
const browserInstance = await getBrowserInstance(
params.logLevel,
false,
params.chromiumOptions ?? {},
params.chromiumOptions,
);

const outputPath = RenderInternals.tmpDir('remotion-render-');
Expand Down Expand Up @@ -172,7 +172,7 @@ const renderHandler = async (
renderId: params.renderId,
}).catch((err) => reject(err));
},
puppeteerInstance: browserInstance,
puppeteerInstance: browserInstance.instance,
serveUrl: params.serveUrl,
jpegQuality: params.jpegQuality ?? RenderInternals.DEFAULT_JPEG_QUALITY,
envVariables: params.envVariables ?? {},
Expand Down Expand Up @@ -269,6 +269,8 @@ const renderHandler = async (
customCredentials: null,
}),
]);
browserInstance.instance.forgetEventLoop();
RenderInternals.Log.verbose('Done!');
return {};
};

Expand Down
31 changes: 17 additions & 14 deletions packages/lambda/src/functions/still.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,25 @@ const innerStillHandler = async ({

const start = Date.now();

const [bucketName, browserInstance] = await Promise.all([
const browserInstancePromise = getBrowserInstance(
lambdaParams.logLevel,
false,
lambdaParams.chromiumOptions,
);
const bucketNamePromise =
lambdaParams.bucketName ??
internalGetOrCreateBucket({
region: getCurrentRegionInFunction(),
enableFolderExpiry: null,
customCredentials: null,
}).then((b) => b.bucketName),
getBrowserInstance(
lambdaParams.logLevel,
false,
lambdaParams.chromiumOptions ?? {},
),
]);
internalGetOrCreateBucket({
region: getCurrentRegionInFunction(),
enableFolderExpiry: null,
customCredentials: null,
}).then((b) => b.bucketName);

const outputDir = RenderInternals.tmpDir('remotion-render-');

const outputPath = path.join(outputDir, 'output');

const region = getCurrentRegionInFunction();
const bucketName = await bucketNamePromise;
const serializedInputPropsWithCustomSchema = await decompressInputProps({
bucketName,
expectedBucketOwner,
Expand All @@ -118,9 +118,10 @@ const innerStillHandler = async ({
offthreadVideoCacheSizeInBytes: lambdaParams.offthreadVideoCacheSizeInBytes,
});

const browserInstance = await browserInstancePromise;
const composition = await validateComposition({
serveUrl,
browserInstance,
browserInstance: browserInstance.instance,
composition: lambdaParams.composition,
serializedInputPropsWithCustomSchema,
envVariables: lambdaParams.envVariables ?? {},
Expand Down Expand Up @@ -184,7 +185,7 @@ const innerStillHandler = async ({
imageFormat: lambdaParams.imageFormat as StillImageFormat,
serializedInputPropsWithCustomSchema,
overwrite: false,
puppeteerInstance: browserInstance,
puppeteerInstance: browserInstance.instance,
jpegQuality:
lambdaParams.jpegQuality ?? RenderInternals.DEFAULT_JPEG_QUALITY,
chromiumOptions: lambdaParams.chromiumOptions,
Expand Down Expand Up @@ -245,6 +246,8 @@ const innerStillHandler = async ({
diskSizeInMb: MAX_EPHEMERAL_STORAGE_IN_MB,
});

browserInstance.instance.forgetEventLoop();

return {
type: 'success' as const,
output: getOutputUrlFromMetadata(
Expand Down
Loading