Add site server process - #19
Conversation
| themeDetails = await server.runPhp( { | ||
| code: themeDetailsPhp, | ||
| } ); |
There was a problem hiding this comment.
runPhp always returns the PHP response as a string:
| import type { WebpackPluginInstance } from 'webpack'; | ||
| const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require( 'fork-ts-checker-webpack-plugin' ); // eslint-disable-line @typescript-eslint/no-var-requires | ||
|
|
||
| const siteServerProcessModulePath = path.resolve( __dirname, '.webpack/main/siteServerProcess.js' ); |
There was a problem hiding this comment.
Not sure if there's a more elegant way to infer the resulting output file.
There was a problem hiding this comment.
I noticed this path is wrong when building the app in production mode. I'll work on a fix before setting the PR ready to review.
| const appDataPath = process.env.STUDIO_APP_DATA_PATH; | ||
| return path.join( appDataPath, process.env.STUDIO_APP_NAME, 'server-files' ); |
There was a problem hiding this comment.
This logic is the same as the one provided in getServerFilesPath but using the values provided via the process' environment variables:
Lines 9 to 12 in 9b54b8c
| return messageId; | ||
| } | ||
|
|
||
| async waitForMessage< T = undefined >( |
There was a problem hiding this comment.
The messaging logic is mostly based on the one used in Playground's web service worker:
https://github.com/WordPress/playground/blob/trunk/packages/php-wasm/web-service-worker/src/messaging.ts
| // whether you're running in development or production). | ||
| declare const SITE_SERVER_PROCESS_MODULE_PATH: string; | ||
|
|
||
| export type MessageName = 'start-server' | 'stop-server' | 'run-php'; |
There was a problem hiding this comment.
If we need to expose more functionality from the server or the PHP runtime, we could introduce it in the future.
There was a problem hiding this comment.
See my suggestion above. I suggest having an object with the events, to avoid typos and magic strings.
| if ( ! isForkedProcess ) { | ||
| // Set the logging path to the default for the platform. | ||
| app.setAppLogsPath(); | ||
| } |
There was a problem hiding this comment.
We only permit configuring the app logs path in the main process since the Electron library is only available there.
|
The failure in the |
|
I have not tested this yet and I will take a closer look tomorrow but I just wanted to say that I really appreciate how detailed your description and explanation is. It is the first time I am working on an Electron app and learning about the infrastructure and architecture of it on the go and it is very helpful to have everything outlined so clearly ❤️ |
Thank you @katinthehatsite 🙂 ! The PR represents a substantial shift in how sites are executed, so I wanted to provide clear context on both the rationale behind it and its implementation. |
|
Note I'd like to cover the refactor and new site server process with unit/e2e tests. However, I'm thinking of doing it in a separate PR to avoid blocking this PR. |
| const { message, messageId, data }: { message: MessageName; messageId: number; data: unknown } = | ||
| messagePayload; | ||
| try { | ||
| switch ( message ) { |
There was a problem hiding this comment.
[Suggestion]:
- Extract messages in order to avoid magic strings; eg:
const messages = { START_SERVER: 'start-server' ...
or
2. Extract messages along with their associated functions; Eg:
const messageHandlers = {
'start-server': start,
'stop-server': stop,
'run-php': runPhp
};
....
try {
const handler = messageHandlers[message];
if (!handler) {
return
}
await handler(message, messageId, data);
} catch (error) {
const errorObj = error instanceof Error ? error : new Error('Unknown Error');
postMessage(errorObj);
}
We can further decouple by extracting the creating a emitMessage function like so:
const emitMessage = (error) => {
const response = { message, messageId };
if (error) {
response['error'] = error;
}
process.parentPort.postMessage(response);
};
and remove it from the handlers like so:
async function runPhp( message: string, messageId: number, data: PHPRunOptions ) {
await server.php.run( data );
}
and use it like so:
const emitMessage = (error = null) => {
const response = { message, messageId };
if (error) {
response['error'] = error;
}
process.parentPort.postMessage(response);
};
try {
const handler = messageHandlers[message];
if (!handler) {
return
}
await handler(message, messageId, data);
emitMessage();
} catch (error) {
const errorObj = error as Error;
if ( ! errorObj ) {
emitMessage( Error( 'Unknown Error' ) );
return;
}
emitMessage( errorObj);
}
| // whether you're running in development or production). | ||
| declare const SITE_SERVER_PROCESS_MODULE_PATH: string; | ||
|
|
||
| export type MessageName = 'start-server' | 'stop-server' | 'run-php'; |
There was a problem hiding this comment.
See my suggestion above. I suggest having an object with the events, to avoid typos and magic strings.
|
@fluiddot I tested it on macOS, and test cases worked as expected: I was able to create a site and start multiple servers, and I noticed a separate process tag in the log file. One thing I noticed is that macOS spawns those processes correctly, but when I stopped all sites, those processes were still lingering there. Is it expected?
|
Good catch @wojtekn. Seems we should manually kill the processes once the server stops. I've pushed a fix in 8e17947. kill-processes-upon-server-stop.mp4 |
| async stop() { | ||
| console.log( 'Stopping server with ID', this.details.id ); | ||
| this.server?.stopServer(); | ||
| await this.server?.stop(); |
There was a problem hiding this comment.
I noticed we weren't waiting for this promise.
|
@kozer I asked for another review just to confirm that the latest changes are aligned with the suggestion you shared here. Since you already approved it as well as @wojtekn, I'm going to proceed with the merge. In case there are any further suggestions, I'll be happy to apply them in a separate PR. Thanks 🙇 ! |
|
@fluiddot , I was afk and just saw your comment! Yes, those changes seem good! Thanks for implementing that! |


Related to https://github.com/Automattic/dotcom-forge/issues/6423.
Proposed Changes
The primary objective of this PR is to decouple the process of running a site from the main thread. This aligns with Playground's concept of orchestrating site execution with an app in the browser. The Studio app faces similar challenges when running a site, where the PHP runtime can impede the main thread and consequently the overall app performance. Therefore, adopting the same approach by separating the process can address this issue.
Key differences between the approach presented here and the app include:
utilityProcessto spawn a separate process. This is akin to Node's worker threads, but the process is managed by Chromium. Initially, I considered using worker threads as suggested in the Electron documentation. However, I encountered several challenges when loading and executing the worker due to the Typescript + Webpack setup. While that approach might be viable, I ultimately opted forutilityProcessdue to its simplicity and similar functionality.Important
Note that the PHP runtime is no longer directly accessible from the app; it is now only accessible by the site server process. However, we provide an API to facilitate running PHP requests.
Changes:
getWpNowPathfunction is utilized in both the main and separate processes. In the latter, the Electron library isn't available, so we rely on process environment variables.wp-nowattempt to find an open port when starting a site. The port selected by the app should take precedence; hence, the logic has been updated to ensure this.Results
Main process performance:
Running the app on Windows
studio-trunk.mp4
studio-separate-thread.mp4
App size
I conducted a comparison of the files generated when running
npm run make:macos-arm64, which I assumed produces the app in production mode. The results indicate that with the changes from this PR, the binary size increases by 1.2 MB (+0.27%), and the zipped installer file grows by 0.4 MB (+0.23%). While this represents a minor increase, it's noteworthy. The primary contributor to this size increase is the generation of a separate file for the site server process child. In the future, it might be worthwhile to explore methods to reduce this size. I have a suspicion that theindex.jsfile generated by webpack includes more code than necessary after relocating certain portions (particularly thewp-nowcode) to the site server process child.Testing Instructions
Performance
Create a site
Start a site
Stop a site (UPDATE)
Electron Helper.Logging in the forked process
npm run maketo generate a build in production mode.~/Library/Logs/Studio.current.log).[main]and[site-server-process]are registered.Pre-merge Checklist