Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/php-wasm/universal/src/lib/php-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,8 @@ export class PHPRequestHandler implements AsyncDisposable {
$_SERVER: this.prepare_$_SERVER_superglobal(
originalRequestUrl,
rewrittenRequestUrl,
scriptPath
scriptPath,
request.protocolVersion
),
body,
scriptPath,
Expand Down Expand Up @@ -863,12 +864,14 @@ export class PHPRequestHandler implements AsyncDisposable {
private prepare_$_SERVER_superglobal(
originalRequestUrl: URL,
rewrittenRequestUrl: URL,
resolvedScriptPath: string
resolvedScriptPath: string,
protocolVersion?: string
): Record<string, string> {
const $_SERVER: Record<string, string> = {
REMOTE_ADDR: '127.0.0.1',
DOCUMENT_ROOT: this.#DOCROOT,
HTTPS: this.#ABSOLUTE_URL.startsWith('https://') ? 'on' : '',
SERVER_PROTOCOL: protocolVersion || 'HTTP/1.1',
};

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/php-wasm/universal/src/lib/universal-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export interface PHPRequest {
* and sent with a `multipart/form-data` header.
*/
body?: string | Uint8Array | Record<string, string | Uint8Array | File>;

/**
* HTTP protocol version string for $_SERVER['SERVER_PROTOCOL'].
* Examples: 'HTTP/1.0', 'HTTP/1.1', 'HTTP/2.0'.
* Default: 'HTTP/1.1'.
*/
protocolVersion?: string;
}

export interface PHPRunOptions {
Expand Down
102 changes: 91 additions & 11 deletions packages/playground/cli/src/run-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
parseDefineNumberArguments,
} from './defines';
import { isPortInUse, startServer } from './start-server';
import { resolveTlsCertificate } from './tls';
import type { PlaygroundCliBlueprintV1Worker } from './blueprints-v1/worker-thread-v1';
import type { PlaygroundCliBlueprintV2Worker } from './blueprints-v2/worker-thread-v2';
import type { XdebugOptions } from '@php-wasm/node';
Expand Down Expand Up @@ -340,6 +341,29 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
'Port to listen on when serving. Defaults to 9400 when available.',
type: 'number',
},
http2: {
describe:
'Enable HTTP/2 with TLS. Requires HTTPS certificates ' +
'(auto-provisioned via mkcert or self-signed if not supplied).',
type: 'boolean',
default: false,
},
'ssl-cert': {
describe:
'Path to a TLS certificate file (PEM format). Used with --http2.',
type: 'string',
},
'ssl-key': {
describe:
'Path to a TLS private key file (PEM format). Used with --http2.',
type: 'string',
},
workers: {
describe:
'Number of PHP worker threads to spawn. Defaults to 6.',
type: 'number',
default: 6,
},
'experimental-multi-worker': {
deprecated:
'This option is not needed. Multiple workers are always used.',
Expand Down Expand Up @@ -436,6 +460,12 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
type: 'boolean',
default: false,
},
// HTTP/2 and TLS
http2: serverOnlyOptions['http2'],
'ssl-cert': serverOnlyOptions['ssl-cert'],
'ssl-key': serverOnlyOptions['ssl-key'],
// Worker pool
workers: serverOnlyOptions['workers'],
// Define constants
define: sharedOptions['define'],
'define-bool': sharedOptions['define-bool'],
Expand Down Expand Up @@ -813,6 +843,12 @@ export interface RunCLIArgs {
'truncate-new-site-directory'?: boolean;
allow?: string;

// --------- Server command args -----------
http2?: boolean;
'ssl-cert'?: string;
'ssl-key'?: string;
workers?: number;

// --------- Start command args -----------
path?: string;
skipBrowser?: boolean;
Expand Down Expand Up @@ -987,33 +1023,49 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
let wordPressReady = false;
let isFirstRequest = true;

const useHttp2 = !!args.http2;
const tlsCertificate = useHttp2
? resolveTlsCertificate({
sslCert: args['ssl-cert'] as string | undefined,
sslKey: args['ssl-key'] as string | undefined,
debug: args.verbosity === 'debug',
})
: undefined;

const server = await startServer({
port: args.port
? args.port
: !(await isPortInUse(selectedPort))
? selectedPort
: 0,
http2: useHttp2,
tlsCertificate,
onBind: async (server: Server, port: number) => {
const host = '127.0.0.1';
const serverUrl = `http://${host}:${port}`;
const scheme = useHttp2 ? 'https' : 'http';
const serverUrl = `${scheme}://${host}:${port}`;
const siteUrl = args['site-url'] || serverUrl;

/**
* With HTTP 1.1, browsers typically support 6 parallel connections per domain.
* With HTTP/1.1, browsers typically support 6 parallel
* connections per domain.
* > browsers open several connections to each domain,
* > sending parallel requests. Default was once 2 to 3 connections,
* > but this has now increased to a more common use of 6 parallel connections.
* > sending parallel requests. Default was once 2 to 3
* > connections, but this has now increased to a more
* > common use of 6 parallel connections.
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x#domain_sharding
*
* While our HTTP server only supports HTTP 1.1 and while we are trying to limit the
* memory requirements of multiple workers, let's hard-code the number of request-handling
* workers to 6.
* Going higher than browsers' max concurrent requests
* seems pointless, and going lower may increase the
* likelihood of deadlock due to workers blocking and
* waiting for file locks.
*
* Going higher than browsers' max concurrent requests seems pointless,
* and going lower may increase the likelihood of deadlock due to workers
* blocking and waiting for file locks.
* With HTTP/2, multiplexing removes the per-domain
* connection limit, so we size the pool based on
* available system memory instead.
*/
const targetWorkerCount = 6;
const targetWorkerCount = (args.workers as number) ?? 6;
warnIfInsufficientMemoryForWorkers(targetWorkerCount);

/*
* Use a real temp dir as a target for the following Playground paths
Expand Down Expand Up @@ -1870,6 +1922,34 @@ function openInBrowser(url: string): void {
});
}

export const ESTIMATED_WORKER_MEMORY_BYTES = 100 * 1024 * 1024; // ~100MB per worker

/**
* Logs a warning when selected worker count appears too high for
* currently free system memory.
*
* Uses 50% of free memory as the recommended worker budget so the system
* has headroom for the OS, browser, and other processes.
*/
export function warnIfInsufficientMemoryForWorkers(workerCount: number): void {
const freeMemory = os.freemem();
const recommendedByMemory = Math.floor(
(freeMemory * 0.5) / ESTIMATED_WORKER_MEMORY_BYTES
);
const freeMemoryMb = Math.round(freeMemory / 1024 / 1024);
if (workerCount > recommendedByMemory) {
logger.warn(
`Selected workers (${workerCount}) may exceed available free memory budget ` +
`(free memory: ${freeMemoryMb}MB, recommended max: ${recommendedByMemory}). ` +
'Continuing with requested worker count.'
);
} else {
logger.log(
`Worker count: ${workerCount} (free memory: ${freeMemoryMb}MB)`
);
}
}

async function zipSite(
playground: Pooled<PlaygroundCliWorker>,
outfile: string
Expand Down
Loading
Loading