parallelize-async-work
ComputerGuy 1 week ago
parent e883cd086b
commit de84f6bea0

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: parallelize more async work

@ -63,7 +63,7 @@ import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */ /** @type {Visitors} */
const visitors = { const visitors = {
_: function set_scope(node, { next, state }) { _: function set_scope(node, { path, next, state }) {
const scope = state.scopes.get(node); const scope = state.scopes.get(node);
if (scope && scope !== state.scope) { if (scope && scope !== state.scope) {
@ -84,6 +84,9 @@ const visitors = {
} else { } else {
next(); next();
} }
if (node.type !== 'VariableDeclaration' && path.at(-1)?.type === 'Program' && state.analysis.instance) {
state.current_parallelized_chunk = null;
}
}, },
AnimateDirective, AnimateDirective,
ArrowFunctionExpression, ArrowFunctionExpression,
@ -176,7 +179,9 @@ export function client_component(analysis, options) {
update: /** @type {any} */ (null), update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null), after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null), template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null) memoizer: /** @type {any} */ (null),
parallelized_derived_chunks: [],
current_parallelized_chunk: null
}; };
const module = /** @type {ESTree.Program} */ ( const module = /** @type {ESTree.Program} */ (

@ -7,7 +7,7 @@ import type {
AssignmentExpression, AssignmentExpression,
UpdateExpression, UpdateExpression,
VariableDeclaration, VariableDeclaration,
Declaration Pattern
} from 'estree'; } from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
@ -83,6 +83,18 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[]; readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */ /** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[]; readonly module_level_snippets: VariableDeclaration[];
readonly parallelized_derived_chunks: ParallelizedChunk[];
current_parallelized_chunk: ParallelizedChunk | null;
}
export interface ParallelizedChunk {
declarators: Array<{
id: Pattern;
init: Expression;
}>;
kind: VariableDeclaration['kind'];
/** index in instance body */
position: number;
} }
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>; export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;

@ -1,7 +1,7 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ /** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */
/** @import { Binding } from '#compiler' */ /** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis, ComponentAnalysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { is_simple_expression } from '../../../utils/ast.js'; import { is_simple_expression } from '../../../utils/ast.js';
@ -15,6 +15,7 @@ import {
import { dev } from '../../../state.js'; import { dev } from '../../../state.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { validate_mutation } from './visitors/shared/utils.js'; import { validate_mutation } from './visitors/shared/utils.js';
import is_reference from 'is-reference';
/** /**
* @param {Binding} binding * @param {Binding} binding
@ -28,6 +29,60 @@ export function is_state_source(binding, analysis) {
); );
} }
/**
* @param {Expression} expression
* @param {Scope} scope
* @param {Analysis | ComponentAnalysis} analysis
* @returns {boolean}
*/
export function can_be_parallelized(expression, scope, analysis) {
let has_closures = false;
/** @type {Set<string>} */
const references = new Set();
walk(expression, null, {
ArrowFunctionExpression(_, { stop }) {
has_closures = true;
stop();
},
FunctionExpression(_, { stop }) {
has_closures = true;
stop();
},
Identifier(node, { path }) {
if (is_reference(node, /** @type {Node} */ (path.at(-1)))) {
references.add(node.name);
}
}
});
if (has_closures) {
return false;
}
for (const reference of references) {
const binding = scope.get(reference);
if (!binding || binding.declaration_kind === 'import') {
return false;
}
if ('template' in analysis) {
if (binding.scope !== analysis.instance.scope) {
return false;
}
} else if (binding.scope !== analysis.module.scope) {
return false;
}
if (binding.kind === 'derived') {
const init = /** @type {CallExpression} */ (binding.initial);
if (analysis.async_deriveds.has(init)) {
return false;
}
}
if (!binding.mutated && !binding.reassigned) {
continue;
}
}
return true;
}
/** /**
* @param {Identifier} node * @param {Identifier} node
* @param {ClientTransformState} state * @param {ClientTransformState} state

@ -5,10 +5,10 @@ import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js'; import { add_state_transformers } from './shared/declarations.js';
/** /**
* @param {Program} _ * @param {Program} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function Program(_, context) { export function Program(node, context) {
if (!context.state.analysis.runes) { if (!context.state.analysis.runes) {
context.state.transform['$$props'] = { context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' }) read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +137,20 @@ export function Program(_, context) {
add_state_transformers(context); add_state_transformers(context);
context.next(); /** @type {Program['body']} */
const body = [];
for (let i = 0; i < node.body.length; i++) {
const transformed = /** @type {Program['body'][number]} */ (context.visit(node.body[i]));
const chunk = context.state.parallelized_derived_chunks?.at(-1);
body.push(transformed);
if (chunk && chunk.position === i) {
const pattern = b.array_pattern(chunk.declarators.map(({ id }) => id));
const init = b.call('$.all', b.array(chunk.declarators.map(({ init }) => init)));
body.push(b.declaration(chunk.kind, [b.declarator(pattern, b.await(init))]));
}
}
return {
...node,
body
};
} }

@ -1,12 +1,18 @@
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { CallExpression, Expression, Identifier, Literal, Program, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */ /** @import { Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext, ParallelizedChunk } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js'; import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js'; import { extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import {
can_be_parallelized,
get_prop_source,
is_prop_source,
is_state_source,
should_proxy
} from '../utils.js';
import { is_hoisted_function } from '../../utils.js'; import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
@ -200,6 +206,18 @@ export function VariableDeclaration(node, context) {
const is_async = context.state.analysis.async_deriveds.has( const is_async = context.state.analysis.async_deriveds.has(
/** @type {CallExpression} */ (init) /** @type {CallExpression} */ (init)
); );
let parallelize = false;
if (
is_async &&
context.state.analysis.instance &&
context.state.scope === context.state.analysis.instance.scope &&
!dev
) {
parallelize = can_be_parallelized(value, context.state.scope, context.state.analysis);
}
/** @type {VariableDeclarator[]} */
const derived_declarators = [];
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ ( let expression = /** @type {Expression} */ (
@ -212,7 +230,7 @@ export function VariableDeclaration(node, context) {
if (is_async) { if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init); const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
let call = b.call( let call = b.call(
'$.async_derived', '$.async_derived' + (parallelize ? '_p' : ''),
b.thunk(expression, true), b.thunk(expression, true),
location ? b.literal(location) : undefined location ? b.literal(location) : undefined
); );
@ -220,14 +238,14 @@ export function VariableDeclaration(node, context) {
call = b.call(b.await(b.call('$.save', call))); call = b.call(b.await(b.call('$.save', call)));
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call)); derived_declarators.push(b.declarator(declarator.id, call));
} else { } else {
if (rune === '$derived') expression = b.thunk(expression); if (rune === '$derived') expression = b.thunk(expression);
let call = b.call('$.derived', expression); let call = b.call('$.derived', expression);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call)); derived_declarators.push(b.declarator(declarator.id, call));
} }
} else { } else {
const init = /** @type {CallExpression} */ (declarator.init); const init = /** @type {CallExpression} */ (declarator.init);
@ -253,7 +271,9 @@ export function VariableDeclaration(node, context) {
b.thunk(expression, true), b.thunk(expression, true),
location ? b.literal(location) : undefined location ? b.literal(location) : undefined
); );
call = b.call(b.await(b.call('$.save', call))); if (!parallelize) {
call = b.call(b.await(b.call('$.save', call)));
}
} }
if (dev) { if (dev) {
@ -261,7 +281,7 @@ export function VariableDeclaration(node, context) {
call = b.call('$.tag', call, b.literal(label)); call = b.call('$.tag', call, b.literal(label));
} }
declarations.push(b.declarator(id, call)); derived_declarators.push(b.declarator(id, call));
} }
const { inserts, paths } = extract_paths(declarator.id, rhs); const { inserts, paths } = extract_paths(declarator.id, rhs);
@ -278,13 +298,13 @@ export function VariableDeclaration(node, context) {
call = b.call('$.tag', call, b.literal(label)); call = b.call('$.tag', call, b.literal(label));
} }
declarations.push(b.declarator(id, call)); derived_declarators.push(b.declarator(id, call));
} }
for (const path of paths) { for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression)); const expression = /** @type {Expression} */ (context.visit(path.expression));
const call = b.call('$.derived', b.thunk(expression)); const call = b.call('$.derived', b.thunk(expression));
declarations.push( derived_declarators.push(
b.declarator( b.declarator(
path.node, path.node,
dev dev
@ -295,6 +315,32 @@ export function VariableDeclaration(node, context) {
} }
} }
if (!parallelize) {
declarations.push(...derived_declarators);
} else if (derived_declarators.length > 0) {
/** @type {ParallelizedChunk['declarators']} */
const declarators = derived_declarators.map(({ id, init }) => ({
id,
init: /** @type {Expression} */ (init)
}));
if (
context.state.current_parallelized_chunk &&
context.state.current_parallelized_chunk.kind === node.kind &&
context.state.current_parallelized_chunk.position ===
/** @type {Program} */ (context.path.at(-1)).body.indexOf(node)
) {
context.state.current_parallelized_chunk.declarators.push(...declarators);
} else {
const chunk = {
kind: node.kind,
declarators,
position: /** @type {Program} */ (context.path.at(-1)).body.indexOf(node)
};
context.state.current_parallelized_chunk = chunk;
context.state.parallelized_derived_chunks.push(chunk);
}
}
continue; continue;
} }
} }

@ -99,6 +99,7 @@ export {
with_script with_script
} from './dom/template.js'; } from './dom/template.js';
export { export {
all,
async_body, async_body,
for_await_track_reactivity_loss, for_await_track_reactivity_loss,
save, save,

@ -189,3 +189,12 @@ export async function async_body(fn) {
unsuspend(); unsuspend();
} }
} }
/**
* @template T
* @param {Array<Promise<T>>} promises
* @returns {Promise<Array<T>>}
*/
export function all(...promises) {
return Promise.all(promises.map((promise) => save(promise).then((restore) => restore())));
}

Loading…
Cancel
Save