Skip to content

Commit 789542b

Browse files
authored
Studio: Improve performance for create site (#1840)
* Add performance logging and optimize site startup with setSiteOptions * Add comprehensive performance logging across entire startup flow * Add detailed performance logs to IPC handlers to track complete site creation flow * Skip updateSiteUrl for newly created sites to save 8 seconds * Revert "Skip updateSiteUrl for newly created sites to save 8 seconds" This reverts commit a0145e8. * performance optimization * performance optimization * performance optimization * performance optimization * increase inactivity timeout * fix race condition in use-site-details * fix unit tests * remove debug logs * playground stop timeout * change thumbnail to async/await syntax * more performance testing * cleanup * display notification after site creatioN * add comment with issue follow-up for extra site start/stop * remove performance logs
1 parent 4c85d61 commit 789542b

File tree

12 files changed

+325
-206
lines changed

12 files changed

+325
-206
lines changed

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT =
7171
WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS * 60 * 60 * 1000; // 6hr
7272

7373
// Playground CLI
74-
export const PLAYGROUND_CLI_INACTIVITY_TIMEOUT = 60 * 1000; // 60 seconds of no output = timeout
74+
export const PLAYGROUND_CLI_INACTIVITY_TIMEOUT = 2 * 60 * 1000; // 2 minutes of no output = timeout
7575
export const PLAYGROUND_CLI_MAX_TIMEOUT = 10 * 60 * 1000; // 10 minutes absolute maximum
7676
export const PLAYGROUND_CLI_ACTIVITY_CHECK_INTERVAL = 5 * 1000; // Check for inactivity every 5 seconds
7777

src/hooks/use-add-site.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,12 @@ export function useAddSite() {
143143
body: __( 'Your new site was imported' ),
144144
} );
145145
} else {
146+
await startServer( newSite.id );
147+
146148
getIpcApi().showNotification( {
147149
title: newSite.name,
148150
body: __( 'Your new site was created' ),
149151
} );
150-
151-
await startServer( newSite.id );
152152
}
153153
}
154154
);

src/hooks/use-site-details.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
153153

154154
const [ data, setData ] = useState< SiteDetails[] >( [] );
155155
const [ loadingSites, setLoadingSites ] = useState< boolean >( true );
156+
const [ addingSiteIds, setAddingSiteIds ] = useState< string[] >( [] );
156157
const firstSite = data[ 0 ] || null;
157158
const [ loadingServer, setLoadingServer ] = useState< Record< string, boolean > >(
158159
firstSite?.id
@@ -236,13 +237,15 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
236237

237238
// Remove the temporary site immediately, but with a minor delay to ensure state updates properly
238239
setTimeout( () => {
240+
setAddingSiteIds( ( prev ) => prev.filter( ( id ) => id !== tempSiteId ) );
239241
setData( ( prevData ) =>
240242
sortSites( prevData.filter( ( site ) => site.id !== tempSiteId ) )
241243
);
242244
}, 2000 );
243245
};
244246

245247
const tempSiteId = crypto.randomUUID();
248+
setAddingSiteIds( ( prev ) => [ ...prev, tempSiteId ] );
246249
setData( ( prevData ) =>
247250
sortSites( [
248251
...prevData,
@@ -259,8 +262,9 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
259262
);
260263
setSelectedSiteId( tempSiteId ); // Set the temporary ID as the selected site
261264

265+
let newSite: SiteDetails;
262266
try {
263-
const newSite = await getIpcApi().createSite( path, {
267+
newSite = await getIpcApi().createSite( path, {
264268
siteName,
265269
wpVersion,
266270
customDomain,
@@ -271,6 +275,10 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
271275
showError( undefined, !! blueprint );
272276
return;
273277
}
278+
setAddingSiteIds( ( prev ) => {
279+
prev.push( newSite.id );
280+
return prev;
281+
} );
274282
// Update the selected site to the new site's ID if the user didn't change it
275283
setSelectedSiteId( ( prevSelectedSiteId ) => {
276284
if ( prevSelectedSiteId === tempSiteId ) {
@@ -281,9 +289,6 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
281289
}
282290
return prevSelectedSiteId;
283291
} );
284-
// It replaces the temporary site created in React
285-
// with the new site generated in the backend, but keeps isAddingSite to true
286-
newSite.isAddingSite = true;
287292
setData( ( prevData ) =>
288293
prevData.map( ( site ) => ( site.id === tempSiteId ? newSite : site ) )
289294
);
@@ -292,15 +297,13 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
292297
await callback( newSite );
293298
}
294299

295-
setData( ( prevData ) =>
296-
prevData.map( ( site ) =>
297-
site.id === newSite.id ? { ...site, isAddingSite: false } : site
298-
)
299-
);
300-
301300
return newSite;
302301
} catch ( error ) {
303302
showError( error, !! blueprint );
303+
} finally {
304+
setAddingSiteIds( ( prev ) =>
305+
prev.filter( ( id ) => id !== tempSiteId && id !== newSite?.id )
306+
);
304307
}
305308
},
306309
[ selectedTab, setSelectedSiteId, setSelectedTab ]
@@ -402,7 +405,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
402405
if ( ! fastDeepEqual( payload.newSites, payload.sites ) ) {
403406
const updatedSites = await getIpcApi().getSiteDetails();
404407
setData( ( prevData ) => {
405-
const tempSite = prevData.find( ( site ) => site.isAddingSite );
408+
const tempSite = prevData.find( ( site ) => addingSiteIds.includes( site.id ) );
406409

407410
if ( ! tempSite ) {
408411
return updatedSites;
@@ -422,7 +425,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
422425
return () => {
423426
unsubscribe();
424427
};
425-
}, [] );
428+
}, [ addingSiteIds ] );
426429

427430
useEffect( () => {
428431
let cancel = false;
@@ -468,9 +471,17 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
468471
setData( data.map( ( site ) => ( site.running ? { ...site, running: false } : site ) ) );
469472
}, [ data ] );
470473

474+
const selectedSite = useMemo( () => {
475+
const site = data.find( ( site ) => site.id === selectedSiteId ) || firstSite;
476+
if ( addingSiteIds.includes( site?.id ) ) {
477+
site.isAddingSite = true;
478+
}
479+
return site;
480+
}, [ addingSiteIds, data, firstSite, selectedSiteId ] );
481+
471482
const context = useMemo(
472483
() => ( {
473-
selectedSite: data.find( ( site ) => site.id === selectedSiteId ) || firstSite,
484+
selectedSite,
474485
data,
475486
setSelectedSiteId,
476487
createSite,
@@ -486,8 +497,8 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
486497
setUploadingSites,
487498
} ),
488499
[
500+
selectedSite,
489501
data,
490-
firstSite,
491502
setSelectedSiteId,
492503
createSite,
493504
updateSite,

src/ipc-handlers.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ export async function importSite(
157157
throw new Error( 'Site not found.' );
158158
}
159159
try {
160+
if ( ! isWordPressDirectory( site.details.path ) ) {
161+
// Workaround to have the necessary WordPress files to run the import - STU-744
162+
await site.start();
163+
await site.stop();
164+
}
165+
160166
const onEvent = ( data: ImportExportEventData ) => {
161167
const parentWindow = BrowserWindow.fromWebContents( event.sender );
162168
sendIpcEventToRendererWithWindow( parentWindow, 'on-import', data, id );
@@ -228,7 +234,7 @@ export async function createSite(
228234
enableHttps,
229235
} as const;
230236

231-
const server = SiteServer.create( details, { wpVersion, blueprint } );
237+
const server = SiteServer.create( details, { wpVersion, blueprint: blueprint?.blueprint } );
232238

233239
if ( ( await pathExists( path ) ) && ( await isEmptyDir( path ) ) ) {
234240
try {
@@ -251,7 +257,6 @@ export async function createSite(
251257
nodePath.join( path, 'wp-config-studio.php' )
252258
);
253259
}
254-
255260
if ( ! ( await pathExists( nodePath.join( path, 'wp-config.php' ) ) ) ) {
256261
await installSqliteIntegration( path );
257262
} else {
@@ -506,12 +511,14 @@ export async function startServer(
506511
} );
507512

508513
if ( server.details.running ) {
509-
try {
510-
await server.updateCachedThumbnail();
511-
await sendThumbnailChangedEvent( event, id );
512-
} catch ( error ) {
513-
console.error( `Failed to update thumbnail for server ${ id }:`, error );
514-
}
514+
void ( async () => {
515+
try {
516+
await server.updateCachedThumbnail();
517+
await sendThumbnailChangedEvent( event, id );
518+
} catch ( error ) {
519+
console.error( `Failed to update thumbnail for server ${ id }:`, error );
520+
}
521+
} )();
515522
}
516523

517524
console.log( `Server started for '${ server.details.name }'` );

src/lib/php-get-theme-details.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,45 @@ export async function phpGetThemeDetails(
1818
throw Error( 'PHP is not instantiated' );
1919
}
2020

21-
const wpLoadPath = getWpLoadPath( server );
22-
23-
const themeDetailsPhp = `<?php
24-
require_once('${ wpLoadPath }');
25-
$theme = wp_get_theme();
26-
echo json_encode([
27-
'name' => $theme->get('Name'),
28-
'path' => $theme->get_stylesheet_directory(),
29-
'slug' => $theme->get_stylesheet(),
30-
'isBlockTheme' => $theme->is_block_theme(),
31-
'supportsWidgets' => current_theme_supports('widgets'),
32-
'supportsMenus' => get_registered_nav_menus() || current_theme_supports('menus'),
33-
]);
34-
`;
35-
3621
try {
22+
// Try to use the persistent mu-plugin API endpoint if available (Playground CLI)
23+
if ( server.php.request ) {
24+
const response = await server.php.request( {
25+
url: '/?studio-admin-api',
26+
method: 'POST',
27+
body: {
28+
action: 'get_theme_details',
29+
},
30+
} );
31+
32+
const themeDetailsParsed = JSON.parse( response.text );
33+
return themeDetailsSchema.parse( themeDetailsParsed );
34+
}
35+
36+
// Fallback to runPhp for WP-Now
37+
const wpLoadPath = getWpLoadPath( server );
38+
39+
const themeDetailsPhp = `<?php
40+
require_once('${ wpLoadPath }');
41+
$theme = wp_get_theme();
42+
echo json_encode([
43+
'name' => $theme->get('Name'),
44+
'path' => $theme->get_stylesheet_directory(),
45+
'slug' => $theme->get_stylesheet(),
46+
'isBlockTheme' => $theme->is_block_theme(),
47+
'supportsWidgets' => current_theme_supports('widgets'),
48+
'supportsMenus' => get_registered_nav_menus() || current_theme_supports('menus'),
49+
]);
50+
`;
51+
3752
const themeDetailsRaw = await server.runPhp( {
3853
code: themeDetailsPhp,
3954
} );
55+
4056
const themeDetailsParsed = JSON.parse( themeDetailsRaw );
4157
return themeDetailsSchema.parse( themeDetailsParsed );
4258
} catch ( error ) {
59+
console.error( 'Failed to get theme details:', error );
4360
return undefined;
4461
}
4562
}

src/lib/wordpress-provider/playground-cli/mu-plugins.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,91 @@ function getStandardMuPlugins( options: Partial< WordPressServerOptions > ): MuP
303303
`,
304304
} );
305305

306+
// Studio Admin API: Persistent endpoint for admin operations
307+
muPlugins.push( {
308+
filename: '0-studio-admin-api.php',
309+
content: `<?php
310+
/**
311+
* Studio Admin API
312+
*
313+
* Provides a persistent endpoint for admin operations that can reuse
314+
* the already-loaded WordPress instance, avoiding the overhead of
315+
* loading wp-load.php multiple times.
316+
*
317+
* This endpoint should only be accessible locally.
318+
*/
319+
320+
add_action( 'init', function() {
321+
// Only handle requests to our endpoint
322+
$is_api_request = isset( $_GET['studio-admin-api'] ) ||
323+
strpos( $_SERVER['REQUEST_URI'] ?? '', 'studio-admin-api' ) !== false;
324+
325+
if ( ! $is_api_request ) {
326+
return;
327+
}
328+
329+
// Security: Only allow POST requests with the correct action
330+
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' || empty( $_POST['action'] ) ) {
331+
status_header( 400 );
332+
header( 'Content-Type: application/json' );
333+
echo json_encode( [ 'error' => 'Invalid request' ] );
334+
exit;
335+
}
336+
337+
$action = $_POST['action'];
338+
$result = null;
339+
340+
switch ( $action ) {
341+
case 'get_theme_details':
342+
$theme = wp_get_theme();
343+
$result = [
344+
'name' => $theme->get('Name'),
345+
'path' => $theme->get_stylesheet_directory(),
346+
'slug' => $theme->get_stylesheet(),
347+
'isBlockTheme' => $theme->is_block_theme(),
348+
'supportsWidgets' => current_theme_supports('widgets'),
349+
'supportsMenus' => get_registered_nav_menus() || current_theme_supports('menus'),
350+
];
351+
break;
352+
353+
case 'set_admin_password':
354+
if ( empty( $_POST['password'] ) ) {
355+
status_header( 400 );
356+
header( 'Content-Type: application/json' );
357+
echo json_encode( [ 'error' => 'Password is required' ] );
358+
exit;
359+
}
360+
361+
$user = get_user_by( 'login', 'admin' );
362+
if ( $user ) {
363+
wp_set_password( $_POST['password'], $user->ID );
364+
} else {
365+
$user_data = array(
366+
'user_login' => 'admin',
367+
'user_pass' => $_POST['password'],
368+
'user_email' => '[email protected]',
369+
'role' => 'administrator',
370+
);
371+
wp_insert_user( $user_data );
372+
}
373+
$result = [ 'success' => true ];
374+
break;
375+
376+
default:
377+
status_header( 400 );
378+
header( 'Content-Type: application/json' );
379+
echo json_encode( [ 'error' => 'Unknown action' ] );
380+
exit;
381+
}
382+
383+
status_header( 200 );
384+
header( 'Content-Type: application/json' );
385+
echo json_encode( $result );
386+
exit;
387+
}, 1 );
388+
`,
389+
} );
390+
306391
return muPlugins;
307392
}
308393

0 commit comments

Comments
 (0)