Skip to content

Commit 07e5356

Browse files
authored
Warn users with long load times (#660)
and tell them to use WEBSITE_RUN_FROM_PACKAGE
1 parent 7cf1eb1 commit 07e5356

File tree

9 files changed

+151
-29
lines changed

9 files changed

+151
-29
lines changed

src/LegacyFunctionLoader.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import { AzFuncSystemError } from './errors';
77
import { loadScriptFile } from './loadScriptFile';
88
import { PackageJson } from './parsers/parsePackageJson';
99
import { nonNullProp } from './utils/nonNull';
10-
import { RegisteredFunction } from './WorkerChannel';
10+
import { RegisteredFunction, WorkerChannel } from './WorkerChannel';
1111

1212
export interface ILegacyFunctionLoader {
13-
load(functionId: string, metadata: rpc.IRpcFunctionMetadata, packageJson: PackageJson): Promise<void>;
13+
load(
14+
channel: WorkerChannel,
15+
functionId: string,
16+
metadata: rpc.IRpcFunctionMetadata,
17+
packageJson: PackageJson
18+
): Promise<void>;
1419
getFunction(functionId: string): RegisteredFunction | undefined;
1520
}
1621

@@ -21,11 +26,16 @@ interface LegacyRegisteredFunction extends RegisteredFunction {
2126
export class LegacyFunctionLoader implements ILegacyFunctionLoader {
2227
#loadedFunctions: { [k: string]: LegacyRegisteredFunction | undefined } = {};
2328

24-
async load(functionId: string, metadata: rpc.IRpcFunctionMetadata, packageJson: PackageJson): Promise<void> {
29+
async load(
30+
channel: WorkerChannel,
31+
functionId: string,
32+
metadata: rpc.IRpcFunctionMetadata,
33+
packageJson: PackageJson
34+
): Promise<void> {
2535
if (metadata.isProxy === true) {
2636
return;
2737
}
28-
const script: any = await loadScriptFile(nonNullProp(metadata, 'scriptFile'), packageJson);
38+
const script: any = await loadScriptFile(channel, nonNullProp(metadata, 'scriptFile'), packageJson);
2939
const entryPoint = <string>(metadata && metadata.entryPoint);
3040
const [callback, thisArg] = getEntryPoint(script, entryPoint);
3141
this.#loadedFunctions[functionId] = { metadata, callback, thisArg };

src/eventHandlers/FunctionLoadHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class FunctionLoadHandler extends EventHandler<'functionLoadRequest', 'fu
3434
const functionId = nonNullProp(msg, 'functionId');
3535
const metadata = nonNullProp(msg, 'metadata');
3636
try {
37-
await channel.legacyFunctionLoader.load(functionId, metadata, channel.packageJson);
37+
await channel.legacyFunctionLoader.load(channel, functionId, metadata, channel.packageJson);
3838
} catch (err) {
3939
const error = ensureErrorType(err);
4040
error.isAzureFunctionsSystemError = true;

src/loadScriptFile.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import * as path from 'path';
45
import * as url from 'url';
6+
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
57
import { AzFuncSystemError } from './errors';
68
import { PackageJson } from './parsers/parsePackageJson';
9+
import { WorkerChannel } from './WorkerChannel';
10+
import LogCategory = rpc.RpcLog.RpcLogCategory;
11+
import LogLevel = rpc.RpcLog.Level;
712

8-
export async function loadScriptFile(filePath: string, packageJson: PackageJson): Promise<unknown> {
9-
let script: unknown;
10-
if (isESModule(filePath, packageJson)) {
11-
const fileUrl = url.pathToFileURL(filePath);
12-
if (fileUrl.href) {
13-
// use eval so it doesn't get compiled into a require()
14-
script = await eval('import(fileUrl.href)');
13+
export async function loadScriptFile(
14+
channel: WorkerChannel,
15+
filePath: string,
16+
packageJson: PackageJson
17+
): Promise<unknown> {
18+
const start = Date.now();
19+
try {
20+
let script: unknown;
21+
if (isESModule(filePath, packageJson)) {
22+
const fileUrl = url.pathToFileURL(filePath);
23+
if (fileUrl.href) {
24+
// use eval so it doesn't get compiled into a require()
25+
script = await eval('import(fileUrl.href)');
26+
} else {
27+
throw new AzFuncSystemError(`'${filePath}' could not be converted to file URL (${fileUrl.href})`);
28+
}
1529
} else {
16-
throw new AzFuncSystemError(`'${filePath}' could not be converted to file URL (${fileUrl.href})`);
30+
script = require(/* webpackIgnore: true */ filePath);
1731
}
18-
} else {
19-
script = require(/* webpackIgnore: true */ filePath);
32+
return script;
33+
} finally {
34+
warnIfLongLoadTime(channel, filePath, start);
35+
}
36+
}
37+
38+
function warnIfLongLoadTime(channel: WorkerChannel, filePath: string, start: number): void {
39+
const timeElapsed = Date.now() - start;
40+
const rfpName = 'WEBSITE_RUN_FROM_PACKAGE';
41+
const rfpValue = process.env[rfpName];
42+
if (timeElapsed > 1000 && (rfpValue === undefined || rfpValue === '0')) {
43+
channel.log({
44+
message: `Loading "${path.basename(filePath)}" took ${timeElapsed}ms`,
45+
level: LogLevel.Warning,
46+
logCategory: LogCategory.System,
47+
});
48+
channel.log({
49+
message: `Set "${rfpName}" to "1" to significantly improve load times. Learn more here: https://aka.ms/AAjon54`,
50+
level: LogLevel.Warning,
51+
logCategory: LogCategory.System,
52+
});
2053
}
21-
return script;
2254
}
2355

2456
export function isESModule(filePath: string, packageJson: PackageJson): boolean {

src/startApp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async function loadEntryPointFile(functionAppDirectory: string, channel: WorkerC
5858
try {
5959
const entryPointFilePath = path.join(functionAppDirectory, file);
6060
channel.currentEntryPoint = entryPointFilePath;
61-
await loadScriptFile(entryPointFilePath, channel.packageJson);
61+
await loadScriptFile(channel, entryPointFilePath, channel.packageJson);
6262
} finally {
6363
channel.currentEntryPoint = undefined;
6464
}

test/LegacyFunctionLoader.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import * as chai from 'chai';
55
import * as chaiAsPromised from 'chai-as-promised';
66
import 'mocha';
77
import * as mock from 'mock-require';
8+
import * as sinon from 'sinon';
89
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
910
import { LegacyFunctionLoader } from '../src/LegacyFunctionLoader';
1011
import { nonNullValue } from '../src/utils/nonNull';
12+
import { WorkerChannel } from '../src/WorkerChannel';
1113
const expect = chai.expect;
1214
chai.use(chaiAsPromised);
1315

1416
describe('LegacyFunctionLoader', () => {
1517
let loader: LegacyFunctionLoader;
1618
let context, logs;
19+
const channel: WorkerChannel = <WorkerChannel>(<any>sinon.createStubInstance(WorkerChannel));
1720

1821
beforeEach(() => {
1922
loader = new LegacyFunctionLoader();
@@ -32,6 +35,7 @@ describe('LegacyFunctionLoader', () => {
3235
mock('test', {});
3336
await expect(
3437
loader.load(
38+
channel,
3539
'functionId',
3640
<rpc.IRpcFunctionMetadata>{
3741
scriptFile: 'test',
@@ -46,6 +50,7 @@ describe('LegacyFunctionLoader', () => {
4650
it('does not load proxy function', async () => {
4751
mock('test', {});
4852
await loader.load(
53+
channel,
4954
'functionId',
5055
<rpc.IRpcFunctionMetadata>{
5156
isProxy: true,
@@ -61,6 +66,7 @@ describe('LegacyFunctionLoader', () => {
6166
const entryPoint = 'wrongEntryPoint';
6267
await expect(
6368
loader.load(
69+
channel,
6470
'functionId',
6571
<rpc.IRpcFunctionMetadata>{
6672
scriptFile: 'test',
@@ -78,6 +84,7 @@ describe('LegacyFunctionLoader', () => {
7884
const entryPoint = 'test';
7985
await expect(
8086
loader.load(
87+
channel,
8188
'functionId',
8289
<rpc.IRpcFunctionMetadata>{
8390
scriptFile: 'test',
@@ -109,6 +116,7 @@ describe('LegacyFunctionLoader', () => {
109116
mock('test', new FuncObject());
110117

111118
await loader.load(
119+
channel,
112120
'functionId',
113121
<rpc.IRpcFunctionMetadata>{
114122
scriptFile: 'test',
@@ -127,6 +135,7 @@ describe('LegacyFunctionLoader', () => {
127135
mock('test', { test: async () => {} });
128136

129137
await loader.load(
138+
channel,
130139
'functionId',
131140
<rpc.IRpcFunctionMetadata>{
132141
scriptFile: 'test',
@@ -146,6 +155,7 @@ describe('LegacyFunctionLoader', () => {
146155
mock('test', { test: async () => {} });
147156

148157
await loader.load(
158+
channel,
149159
'functionId',
150160
<rpc.IRpcFunctionMetadata>{
151161
scriptFile: 'test',

test/eventHandlers/FunctionLoadHandler.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as sinon from 'sinon';
66
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
77
import { LegacyFunctionLoader } from '../../src/LegacyFunctionLoader';
88
import { PackageJson } from '../../src/parsers/parsePackageJson';
9+
import { WorkerChannel } from '../../src/WorkerChannel';
910
import { beforeEventHandlerSuite } from './beforeEventHandlerSuite';
1011
import { TestEventStream } from './TestEventStream';
1112
import LogCategory = rpc.RpcLog.RpcLogCategory;
@@ -58,7 +59,9 @@ describe('FunctionLoadHandler', () => {
5859

5960
const originalLoader = loader.load;
6061
try {
61-
loader.load = sinon.stub<[string, rpc.IRpcFunctionMetadata, PackageJson], Promise<void>>().throws(err);
62+
loader.load = sinon
63+
.stub<[WorkerChannel, string, rpc.IRpcFunctionMetadata, PackageJson], Promise<void>>()
64+
.throws(err);
6265

6366
stream.addTestMessage({
6467
requestId: 'id',

test/eventHandlers/TestEventStream.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { expect } from 'chai';
55
import { EventEmitter } from 'events';
6+
import * as path from 'path';
67
import * as sinon from 'sinon';
78
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
89
import { IEventStream } from '../../src/GrpcClient';
@@ -41,9 +42,12 @@ export class TestEventStream extends EventEmitter implements IEventStream {
4142
const calls = this.written.getCalls();
4243

4344
// First, validate the "shortened" form of the messages. This will result in a more readable error for most test failures
44-
const shortExpectedMsgs = expectedMsgs.map(getShortenedMsg);
45-
const shortActualMsgs = calls.map((c) => getShortenedMsg(c.args[0]));
46-
expect(shortActualMsgs).to.deep.equal(shortExpectedMsgs);
45+
if (!expectedMsgs.find((m) => m instanceof RegExpStreamingMessage)) {
46+
// shortened message won't work if it's a regexp
47+
const shortExpectedMsgs = expectedMsgs.map(getShortenedMsg);
48+
const shortActualMsgs = calls.map((c) => getShortenedMsg(c.args[0]));
49+
expect(shortActualMsgs).to.deep.equal(shortExpectedMsgs);
50+
}
4751

4852
// Next, do a more comprehensive check on the messages
4953
expect(calls.length).to.equal(
@@ -82,6 +86,10 @@ export class TestEventStream extends EventEmitter implements IEventStream {
8286
}
8387
Object.assign(process.env, this.originalEnv);
8488

89+
// Reset require cache for entryPoint files that depend on timing
90+
const filePath = path.join(__dirname, 'entryPointFiles/longLoad.js');
91+
delete require.cache[require.resolve(filePath)];
92+
8593
// minor delay so that it's more likely extraneous messages are associated with this test as opposed to leaking into the next test
8694
await new Promise((resolve) => setTimeout(resolve, 20));
8795
await this.assertCalledWith();

test/eventHandlers/WorkerInitHandler.test.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,28 @@ export namespace Msg {
109109
};
110110
}
111111

112-
export function warning(message: string): rpc.IStreamingMessage {
113-
return {
114-
rpcLog: {
115-
message,
116-
level: LogLevel.Warning,
117-
logCategory: LogCategory.System,
118-
},
119-
};
112+
export function warning(message: string | RegExp): RegExpStreamingMessage | rpc.IStreamingMessage {
113+
if (typeof message === 'string') {
114+
return {
115+
rpcLog: {
116+
message,
117+
level: LogLevel.Warning,
118+
logCategory: LogCategory.System,
119+
},
120+
};
121+
} else {
122+
return new RegExpStreamingMessage(
123+
{
124+
rpcLog: {
125+
level: LogLevel.Warning,
126+
logCategory: LogCategory.System,
127+
},
128+
},
129+
{
130+
'rpcLog.message': message,
131+
}
132+
);
133+
}
120134
}
121135

122136
export function error(message: string): rpc.IStreamingMessage {
@@ -321,4 +335,47 @@ describe('WorkerInitHandler', () => {
321335
Msg.response
322336
);
323337
});
338+
339+
for (const rfpValue of ['1', 'https://url']) {
340+
it(`Skips warn for long load time if rfp already set to ${rfpValue}`, async () => {
341+
const fileName = 'entryPointFiles/longLoad.js';
342+
mockFs({
343+
[__dirname]: {
344+
'package.json': JSON.stringify({ main: fileName }),
345+
entryPointFiles: mockFs.load(path.join(__dirname, 'entryPointFiles')),
346+
},
347+
});
348+
349+
process.env.WEBSITE_RUN_FROM_PACKAGE = rfpValue;
350+
stream.addTestMessage(Msg.init(__dirname));
351+
await stream.assertCalledWith(
352+
Msg.receivedInitLog,
353+
Msg.loadingEntryPoint(fileName),
354+
Msg.loadedEntryPoint(fileName),
355+
Msg.response
356+
);
357+
});
358+
}
359+
360+
it('Warns for long load time', async () => {
361+
const fileName = 'entryPointFiles/longLoad.js';
362+
mockFs({
363+
[__dirname]: {
364+
'package.json': JSON.stringify({ main: fileName }),
365+
entryPointFiles: mockFs.load(path.join(__dirname, 'entryPointFiles')),
366+
},
367+
});
368+
369+
stream.addTestMessage(Msg.init(__dirname));
370+
await stream.assertCalledWith(
371+
Msg.receivedInitLog,
372+
Msg.loadingEntryPoint(fileName),
373+
Msg.warning(/Loading "longLoad.js" took [0-9]+ms/),
374+
Msg.warning(
375+
'Set "WEBSITE_RUN_FROM_PACKAGE" to "1" to significantly improve load times. Learn more here: https://aka.ms/AAjon54'
376+
),
377+
Msg.loadedEntryPoint(fileName),
378+
Msg.response
379+
);
380+
});
324381
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const start = Date.now();
2+
while (Date.now() < start + 1001) {}

0 commit comments

Comments
 (0)