Skip to content

Commit 8fcd96a

Browse files
committed
Move log activation off the critical extension activation path
Initializing both the file transport and containing folder for the log files was blocking extension activation, sometimes taking up to 1,500ms. Move log activation off the critical path of extension activation by avoiding awaiting promises before continuing activation. Any logs logged before the file transport is ready are buffered and replayed into the file transport once file system operations are complete. This cuts extension activation time down by about 50%. This patch also adds timing metrics to extension startup, printing them to the logs, and also adds a new transport to print to the console when doing extension development.
1 parent 1231fa8 commit 8fcd96a

File tree

4 files changed

+170
-31
lines changed

4 files changed

+170
-31
lines changed

src/WorkspaceContext.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,12 @@ export class WorkspaceContext implements vscode.Disposable {
350350
// add workspace folders, already loaded
351351
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
352352
for (const folder of vscode.workspace.workspaceFolders) {
353+
const singleFolderStartTime = Date.now();
353354
await this.addWorkspaceFolder(folder);
355+
const singleFolderElapsed = Date.now() - singleFolderStartTime;
356+
this.logger.info(
357+
`Added workspace folder ${folder.name} in ${singleFolderElapsed}ms`
358+
);
354359
}
355360
}
356361

@@ -444,20 +449,38 @@ export class WorkspaceContext implements vscode.Disposable {
444449
* @param folder folder being added
445450
*/
446451
async addWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) {
452+
const searchStartTime = Date.now();
447453
const folders = await searchForPackages(
448454
workspaceFolder.uri,
449455
configuration.disableSwiftPMIntegration,
450456
configuration.folder(workspaceFolder).searchSubfoldersForPackages,
451457
configuration.folder(workspaceFolder).ignoreSearchingForPackagesInSubfolders,
452458
this.globalToolchainSwiftVersion
453459
);
460+
const searchElapsed = Date.now() - searchStartTime;
461+
this.logger.info(
462+
`Package search for ${workspaceFolder.name} completed in ${searchElapsed}ms (found ${folders.length} packages)`
463+
);
454464

465+
const addPackagesStartTime = Date.now();
455466
for (const folder of folders) {
467+
const singlePackageStartTime = Date.now();
456468
await this.addPackageFolder(folder, workspaceFolder);
469+
const singlePackageElapsed = Date.now() - singlePackageStartTime;
470+
this.logger.info(`Added package folder ${folder.fsPath} in ${singlePackageElapsed}ms`);
457471
}
472+
const addPackagesElapsed = Date.now() - addPackagesStartTime;
473+
this.logger.info(
474+
`All package folders for ${workspaceFolder.name} added in ${addPackagesElapsed}ms`
475+
);
458476

459477
if (this.getActiveWorkspaceFolder(vscode.window.activeTextEditor) === workspaceFolder) {
478+
const focusStartTime = Date.now();
460479
await this.focusTextEditor(vscode.window.activeTextEditor);
480+
const focusElapsed = Date.now() - focusStartTime;
481+
this.logger.info(
482+
`Focus text editor for ${workspaceFolder.name} completed in ${focusElapsed}ms`
483+
);
461484
}
462485
}
463486

src/extension.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,28 @@ export interface Api {
5454
* Activate the extension. This is the main entry point.
5555
*/
5656
export async function activate(context: vscode.ExtensionContext): Promise<Api> {
57+
const activationStartTime = Date.now();
5758
try {
58-
await vscode.workspace.fs.createDirectory(context.logUri);
59-
const logger = new SwiftLoggerFactory(context.logUri).create(
60-
"Swift",
61-
"swift-vscode-extension.log"
62-
);
63-
context.subscriptions.push(logger);
59+
const logSetupStartTime = Date.now();
60+
const logger = configureLogging(context);
61+
const logSetupElapsed = Date.now() - logSetupStartTime;
6462
logger.info(
6563
`Activating Swift for Visual Studio Code ${context.extension.packageJSON.version}...`
6664
);
65+
logger.info(`Log setup completed in ${logSetupElapsed}ms`);
6766

67+
const preToolchainStartTime = Date.now();
6868
checkAndWarnAboutWindowsSymlinks(logger);
6969

7070
const contextKeys = createContextKeys();
71+
const preToolchainElapsed = Date.now() - preToolchainStartTime;
72+
const toolchainStartTime = Date.now();
7173
const toolchain = await createActiveToolchain(context, contextKeys, logger);
74+
const toolchainElapsed = Date.now() - toolchainStartTime;
75+
76+
const swiftlyCheckStartTime = Date.now();
7277
checkForSwiftlyInstallation(contextKeys, logger);
78+
const swiftlyCheckElapsed = Date.now() - swiftlyCheckStartTime;
7379

7480
// If we don't have a toolchain, show an error and stop initializing the extension.
7581
// This can happen if the user has not installed Swift or if the toolchain is not
@@ -95,9 +101,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
95101
}
96102
}
97103

104+
const workspaceContextStartTime = Date.now();
98105
const workspaceContext = new WorkspaceContext(context, contextKeys, logger, toolchain);
99106
context.subscriptions.push(workspaceContext);
107+
const workspaceContextElapsed = Date.now() - workspaceContextStartTime;
100108

109+
const subscriptionsStartTime = Date.now();
101110
context.subscriptions.push(new SwiftEnvironmentVariablesManager(context));
102111
context.subscriptions.push(SwiftTerminalProfileProvider.register());
103112
context.subscriptions.push(
@@ -152,12 +161,22 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
152161
context.subscriptions.push(TestExplorer.observeFolders(workspaceContext));
153162

154163
context.subscriptions.push(registerSourceKitSchemaWatcher(workspaceContext));
164+
const subscriptionsElapsed = Date.now() - subscriptionsStartTime;
155165

156166
// setup workspace context with initial workspace folders
157-
void workspaceContext.addWorkspaceFolders();
167+
const workspaceFoldersStartTime = Date.now();
168+
await workspaceContext.addWorkspaceFolders();
169+
const workspaceFoldersElapsed = Date.now() - workspaceFoldersStartTime;
158170

171+
const finalStepsStartTime = Date.now();
159172
// Mark the extension as activated.
160173
contextKeys.isActivated = true;
174+
const finalStepsElapsed = Date.now() - finalStepsStartTime;
175+
176+
const totalActivationTime = Date.now() - activationStartTime;
177+
logger.info(
178+
`Extension activation completed in ${totalActivationTime}ms (log-setup: ${logSetupElapsed}ms, pre-toolchain: ${preToolchainElapsed}ms, toolchain: ${toolchainElapsed}ms, swiftly-check: ${swiftlyCheckElapsed}ms, workspace-context: ${workspaceContextElapsed}ms, subscriptions: ${subscriptionsElapsed}ms, workspace-folders: ${workspaceFoldersElapsed}ms, final-steps: ${finalStepsElapsed}ms)`
179+
);
161180

162181
return {
163182
workspaceContext,
@@ -177,6 +196,26 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
177196
}
178197
}
179198

199+
function configureLogging(context: vscode.ExtensionContext) {
200+
// Create log directory asynchronously but don't await it to avoid blocking activation
201+
const logDirPromise = vscode.workspace.fs.createDirectory(context.logUri);
202+
203+
const logger = new SwiftLoggerFactory(context.logUri).create(
204+
"Swift",
205+
"swift-vscode-extension.log"
206+
);
207+
context.subscriptions.push(logger);
208+
209+
void Promise.resolve(logDirPromise)
210+
.then(() => {
211+
// File transport will be added when directory is ready
212+
})
213+
.catch((error: unknown) => {
214+
logger.warn(`Failed to create log directory: ${error}`);
215+
});
216+
return logger;
217+
}
218+
180219
function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise<void> {
181220
// function called when a folder is added. I broke this out so we can trigger it
182221
// without having to await for it.

src/logging/SwiftLogger.ts

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
// SPDX-License-Identifier: Apache-2.0
1212
//
1313
//===----------------------------------------------------------------------===//
14+
import * as fs from "fs";
1415
import * as vscode from "vscode";
1516
import * as winston from "winston";
1617

1718
import configuration from "../configuration";
18-
import { IS_RUNNING_UNDER_TEST } from "../utilities/utilities";
19+
import { IS_RUNNING_IN_DEVELOPMENT_MODE, IS_RUNNING_UNDER_TEST } from "../utilities/utilities";
1920
import { OutputChannelTransport } from "./OutputChannelTransport";
2021
import { RollingLog } from "./RollingLog";
2122
import { RollingLogTransport } from "./RollingLogTransport";
@@ -31,6 +32,8 @@ export class SwiftLogger implements vscode.Disposable {
3132
private logger: winston.Logger;
3233
protected rollingLog: RollingLog;
3334
protected outputChannel: vscode.OutputChannel;
35+
private fileTransportReady = false;
36+
private pendingLogs: Array<{ level: string; message: string; meta?: LoggerMeta }> = [];
3437

3538
constructor(
3639
public readonly name: string,
@@ -42,16 +45,19 @@ export class SwiftLogger implements vscode.Disposable {
4245
const ouptutChannelTransport = new OutputChannelTransport(this.outputChannel);
4346
ouptutChannelTransport.level = this.outputChannelLevel;
4447
const rollingLogTransport = new RollingLogTransport(this.rollingLog);
48+
49+
// Create logger with minimal transports initially for faster startup
50+
const initialTransports = [
51+
ouptutChannelTransport,
52+
// Only want to capture the rolling log in memory when testing
53+
...(IS_RUNNING_UNDER_TEST ? [rollingLogTransport] : []),
54+
...(IS_RUNNING_IN_DEVELOPMENT_MODE
55+
? [new winston.transports.Console({ level: "debug" })]
56+
: []),
57+
];
58+
4559
this.logger = winston.createLogger({
46-
transports: [
47-
new winston.transports.File({
48-
filename: this.logFilePath,
49-
level: "debug", // File logging at the 'debug' level always
50-
}),
51-
ouptutChannelTransport,
52-
// Only want to capture the rolling log in memory when testing
53-
...(IS_RUNNING_UNDER_TEST ? [rollingLogTransport] : []),
54-
],
60+
transports: initialTransports,
5561
format: winston.format.combine(
5662
winston.format.errors({ stack: true }),
5763
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), // This is the format of `vscode.LogOutputChannel`
@@ -61,6 +67,43 @@ export class SwiftLogger implements vscode.Disposable {
6167
winston.format.colorize()
6268
),
6369
});
70+
71+
// Add file transport asynchronously to avoid blocking startup
72+
setTimeout(() => {
73+
try {
74+
const fileTransport = new winston.transports.File({
75+
filename: this.logFilePath,
76+
level: "debug", // File logging at the 'debug' level always
77+
});
78+
79+
// Add the file transport to the main logger
80+
this.logger.add(fileTransport);
81+
this.fileTransportReady = true;
82+
83+
// Write buffered logs directly to the file using Node.js fs
84+
if (this.pendingLogs.length > 0) {
85+
const logEntries =
86+
this.pendingLogs
87+
.map(({ level, message }) => {
88+
const timestamp = new Date()
89+
.toISOString()
90+
.replace("T", " ")
91+
.replace("Z", "");
92+
return `${timestamp} [${level}] ${message}`;
93+
})
94+
.join("\n") + "\n";
95+
96+
fs.appendFileSync(this.logFilePath, logEntries);
97+
}
98+
99+
this.pendingLogs = []; // Clear the buffer
100+
} catch (error) {
101+
// If file transport fails, continue with output channel only
102+
this.logger.warn(`Failed to initialize file logging: ${error}`);
103+
this.fileTransportReady = true; // Mark as ready even if failed to stop buffering
104+
this.pendingLogs = []; // Clear the buffer
105+
}
106+
}, 0);
64107
this.disposables.push(
65108
{
66109
dispose: () => {
@@ -78,30 +121,50 @@ export class SwiftLogger implements vscode.Disposable {
78121
e.affectsConfiguration("swift.outputChannelLogLevel") ||
79122
e.affectsConfiguration("swift.diagnostics")
80123
) {
124+
// Clear cache when configuration changes
125+
this._cachedOutputChannelLevel = undefined;
81126
ouptutChannelTransport.level = this.outputChannelLevel;
82127
}
83128
})
84129
);
85130
}
86131

87132
debug(message: LoggerMeta, label?: string, options?: LogMessageOptions) {
88-
this.logger.debug(this.normalizeMessage(message, label), options);
133+
const normalizedMessage = this.normalizeMessage(message, label);
134+
this.logWithBuffer("debug", normalizedMessage, options);
89135
}
90136

91137
info(message: LoggerMeta, label?: string, options?: LogMessageOptions) {
92-
this.logger.info(this.normalizeMessage(message, label), options);
138+
const normalizedMessage = this.normalizeMessage(message, label);
139+
this.logWithBuffer("info", normalizedMessage, options);
93140
}
94141

95142
warn(message: LoggerMeta, label?: string, options?: LogMessageOptions) {
96-
this.logger.warn(this.normalizeMessage(message, label), options);
143+
const normalizedMessage = this.normalizeMessage(message, label);
144+
this.logWithBuffer("warn", normalizedMessage, options);
97145
}
98146

99147
error(message: LoggerMeta, label?: string, options?: LogMessageOptions) {
100148
if (message instanceof Error) {
101-
this.logger.error(message);
149+
this.logWithBuffer("error", message);
102150
return;
103151
}
104-
this.logger.error(this.normalizeMessage(message, label), options);
152+
const normalizedMessage = this.normalizeMessage(message, label);
153+
this.logWithBuffer("error", normalizedMessage, options);
154+
}
155+
156+
private logWithBuffer(level: string, message: string | Error, meta?: LoggerMeta) {
157+
// Always log to current transports (output channel, console, etc.)
158+
if (message instanceof Error) {
159+
this.logger.log(level, message);
160+
} else {
161+
this.logger.log(level, message, meta);
162+
}
163+
164+
// If file transport isn't ready yet, buffer the log for replay
165+
if (!this.fileTransportReady) {
166+
this.pendingLogs.push({ level, message: message.toString(), meta });
167+
}
105168
}
106169

107170
get logs(): string[] {
@@ -130,17 +193,25 @@ export class SwiftLogger implements vscode.Disposable {
130193
return fullMessage;
131194
}
132195

196+
private _cachedOutputChannelLevel: string | undefined;
197+
133198
private get outputChannelLevel(): string {
134-
const info = vscode.workspace.getConfiguration("swift").inspect("outputChannelLogLevel");
135-
// If the user has explicitly set `outputChannelLogLevel` then use it, otherwise
136-
// check the deprecated `diagnostics` property
137-
if (info?.globalValue || info?.workspaceValue || info?.workspaceFolderValue) {
138-
return configuration.outputChannelLogLevel;
139-
} else if (configuration.diagnostics) {
140-
return "debug";
141-
} else {
142-
return configuration.outputChannelLogLevel;
199+
// Cache the configuration lookup to avoid repeated expensive calls during initialization
200+
if (this._cachedOutputChannelLevel === undefined) {
201+
const info = vscode.workspace
202+
.getConfiguration("swift")
203+
.inspect("outputChannelLogLevel");
204+
// If the user has explicitly set `outputChannelLogLevel` then use it, otherwise
205+
// check the deprecated `diagnostics` property
206+
if (info?.globalValue || info?.workspaceValue || info?.workspaceFolderValue) {
207+
this._cachedOutputChannelLevel = configuration.outputChannelLogLevel;
208+
} else if (configuration.diagnostics) {
209+
this._cachedOutputChannelLevel = "debug";
210+
} else {
211+
this._cachedOutputChannelLevel = configuration.outputChannelLogLevel;
212+
}
143213
}
214+
return this._cachedOutputChannelLevel;
144215
}
145216

146217
dispose() {

src/utilities/utilities.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export const IS_RUNNING_UNDER_DOCKER = IS_RUNNING_UNDER_ACT || IS_RUNNING_UNDER_
5050
*/
5151
export const IS_RUNNING_UNDER_TEST = process.env.RUNNING_UNDER_VSCODE_TEST_CLI === "1";
5252

53+
/**
54+
* Determined by the presence of the `VSCODE_DEBUG` environment variable, set by the
55+
* launch.json when starting the extension in development.
56+
*/
57+
export const IS_RUNNING_IN_DEVELOPMENT_MODE = process.env["VSCODE_DEBUG"] === "1";
58+
5359
/** Determines whether the provided object has any properties set to non-null values. */
5460
export function isEmptyObject(obj: { [key: string]: unknown }): boolean {
5561
const properties = Object.getOwnPropertyNames(obj).filter(

0 commit comments

Comments
 (0)