pull/16688/merge
ComputerGuy 4 days ago committed by GitHub
commit bbeb26474a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

@ -176,7 +176,9 @@ export function client_component(analysis, options) {
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null)
memoizer: /** @type {any} */ (null),
parallelized_chunks: [],
current_parallelized_chunk: null
};
const module = /** @type {ESTree.Program} */ (

@ -7,9 +7,9 @@ import type {
AssignmentExpression,
UpdateExpression,
VariableDeclaration,
Declaration
Pattern
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js';
@ -83,6 +83,20 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
/** async deriveds and certain awaited variables are chunked so they can be parallelized via `Promise.all` */
readonly parallelized_chunks: ParallelizedChunk[];
current_parallelized_chunk: ParallelizedChunk | null;
}
export interface ParallelizedChunk {
declarators: Array<{
id: Pattern | null;
init: Expression;
}>;
kind: VariableDeclaration['kind'] | null;
/** index in instance body */
position: number;
bindings: Binding[];
}
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 { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
import { is_simple_expression } from '../../../utils/ast.js';
@ -15,6 +15,7 @@ import {
import { dev } from '../../../state.js';
import { walk } from 'zimmerframe';
import { validate_mutation } from './visitors/shared/utils.js';
import is_reference from 'is-reference';
/**
* @param {Binding} binding
@ -28,6 +29,79 @@ export function is_state_source(binding, analysis) {
);
}
/**
* @param {Expression} expression
* @param {Scope} scope
* @param {Analysis | ComponentAnalysis} analysis
* @param {Binding[]} bindings bindings currently being parallelized (and cannot be accessed)
* @returns {boolean}
*/
export function can_be_parallelized(expression, scope, analysis, bindings) {
let has_closures = false;
let should_stop = false;
/** @type {Set<string>} */
const references = new Set();
walk(/** @type {Node} */ (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);
}
},
MemberExpression(node, { stop }) {
should_stop = true;
stop();
},
CallExpression(node, { stop }) {
should_stop = true;
stop();
},
NewExpression(node, { stop }) {
should_stop = true;
stop();
},
StaticBlock(node, { stop }) {
has_closures = true;
stop();
}
});
if (has_closures || should_stop) {
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 (bindings.includes(binding)) {
return false;
}
if (binding.kind === 'derived') {
const init = /** @type {CallExpression} */ (binding.initial);
if (analysis.async_deriveds.has(init)) {
return false;
}
}
}
return true;
}
/**
* @param {Identifier} node
* @param {ClientTransformState} state

@ -1,13 +1,17 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
/** @import { Expression, ExpressionStatement, Node, Program } from 'estree' */
/** @import { ComponentContext, ParallelizedChunk } from '../types' */
import * as b from '#compiler/builders';
import { is_expression_async } from '../../../../utils/ast.js';
import { get_rune } from '../../../scope.js';
import { can_be_parallelized } from '../utils.js';
/**
* @param {ExpressionStatement} node
* @param {ComponentContext} context
*/
export function ExpressionStatement(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
const position = /** @type {Program} */ (parent).body?.indexOf?.(node);
if (node.expression.type === 'CallExpression') {
const rune = get_rune(node.expression, context.state.scope);
@ -25,6 +29,40 @@ export function ExpressionStatement(node, context) {
return b.empty;
}
}
if (
node.expression.type === 'AwaitExpression' &&
!is_expression_async(node.expression.argument) &&
context.state.analysis.instance?.scope === context.state.scope
) {
const current_chunk = context.state.current_parallelized_chunk;
const parallelize = can_be_parallelized(
node.expression.argument,
context.state.scope,
context.state.analysis,
current_chunk?.bindings ?? []
);
if (parallelize) {
const declarator = {
id: null,
init: /** @type {Expression} */ (context.visit(node.expression.argument))
};
if (current_chunk) {
current_chunk.declarators.push(declarator);
current_chunk.position = position;
} else {
/** @type {ParallelizedChunk} */
const chunk = {
kind: null,
declarators: [declarator],
position,
bindings: []
};
context.state.current_parallelized_chunk = chunk;
context.state.parallelized_chunks.push(chunk);
}
return b.empty;
}
}
context.next();
}

@ -1,12 +1,12 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types' */
/** @import { ComponentContext } from '../types' */
import is_reference from 'is-reference';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
/**
* @param {Identifier} node
* @param {Context} context
* @param {ComponentContext} context
*/
export function Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
@ -35,6 +35,9 @@ export function Identifier(node, context) {
return b.id('$$props');
}
}
if (binding && context.state.current_parallelized_chunk?.bindings?.includes(binding)) {
context.state.current_parallelized_chunk = null;
}
return build_getter(node, context.state);
}

@ -5,10 +5,10 @@ import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {Program} _
* @param {Program} node
* @param {ComponentContext} context
*/
export function Program(_, context) {
export function Program(node, context) {
if (!context.state.analysis.runes) {
context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +137,52 @@ export function Program(_, 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]));
body.push(transformed);
}
if (context.state.parallelized_chunks) {
let offset = 0;
for (const chunk of context.state.parallelized_chunks) {
if (chunk.declarators.length === 1) {
const declarator = chunk.declarators[0];
if (declarator.id === null || chunk.kind === null) {
body.splice(
chunk.position + offset,
0,
b.stmt(b.call(b.await(b.call('$.save', declarator.init))))
);
} else {
body.splice(
chunk.position + offset,
0,
b.declaration(chunk.kind, [
b.declarator(declarator.id, b.call(b.await(b.call('$.save', declarator.init))))
])
);
}
} else {
const pattern = b.array_pattern(chunk.declarators.map(({ id }) => id));
const init = b.call(
b.await(b.call('$.save', b.call('$.all', ...chunk.declarators.map(({ init }) => init))))
);
if (pattern.elements.every((element) => element === null)) {
body.splice(chunk.position + offset, 0, b.stmt(init));
} else {
body.splice(
chunk.position + offset,
0,
b.declaration(chunk.kind ?? 'const', [b.declarator(pattern, init)])
);
}
}
offset++;
}
}
return {
...node,
body
};
}

@ -1,12 +1,18 @@
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { AwaitExpression, CallExpression, Expression, Identifier, Literal, Node, Program, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ComponentContext, ParallelizedChunk } from '../types' */
import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import { extract_paths, is_expression_async } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.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 { get_value } from './shared/declarations.js';
@ -17,12 +23,14 @@ import { get_value } from './shared/declarations.js';
export function VariableDeclaration(node, context) {
/** @type {VariableDeclarator[]} */
const declarations = [];
const parent = /** @type {Node} */ (context.path.at(-1));
const position = /** @type {Program} */ (parent).body?.indexOf?.(node);
if (context.state.analysis.runes) {
for (const declarator of node.declarations) {
const init = /** @type {Expression} */ (declarator.init);
const rune = get_rune(init, context.state.scope);
const bindings = context.state.scope.get_bindings(declarator);
if (
!rune ||
rune === '$effect.tracking' ||
@ -39,6 +47,51 @@ export function VariableDeclaration(node, context) {
continue;
}
if (
init?.type === 'AwaitExpression' &&
context.state.analysis.instance?.scope === context.state.scope &&
!is_expression_async(init.argument)
) {
const current_chunk = context.state.current_parallelized_chunk;
const parallelize = can_be_parallelized(
init.argument,
context.state.scope,
context.state.analysis,
[...(current_chunk?.bindings ?? []), ...bindings]
);
if (parallelize) {
const { id, init: visited_init } = /** @type {VariableDeclarator} */ (
context.visit({
...declarator,
init: init.argument
})
);
const _declarator = {
id,
init: /** @type {Expression} */ (visited_init)
};
if (
current_chunk &&
(current_chunk.kind === node.kind || current_chunk.kind === null)
) {
current_chunk.declarators.push(_declarator);
current_chunk.bindings.push(...bindings);
current_chunk.position = /** @type {Program} */ (parent).body.indexOf(node);
current_chunk.kind = node.kind;
} else {
/** @type {ParallelizedChunk} */
const chunk = {
kind: node.kind,
declarators: [_declarator],
position,
bindings
};
context.state.current_parallelized_chunk = chunk;
context.state.parallelized_chunks.push(chunk);
}
continue;
}
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
@ -124,47 +177,85 @@ export function VariableDeclaration(node, context) {
const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
if (rune === '$state' || rune === '$state.raw') {
const state_declarators = [];
const current_chunk = context.state.current_parallelized_chunk;
const parallelize =
declarator.id.type === 'Identifier' &&
context.state.analysis.instance?.scope === context.state.scope &&
value.type === 'AwaitExpression' &&
!is_expression_async(value.argument) &&
can_be_parallelized(value.argument, context.state.scope, context.state.analysis, [
...(current_chunk?.bindings ?? []),
...bindings
]);
/**
* @param {Identifier} id
* @param {Expression} visited
* @param {Expression} value
*/
const create_state_declarator = (id, value) => {
const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(id.name)
);
const create_state_declarator = (id, visited, value) => {
const binding = /** @type {Binding} */ (context.state.scope.get(id.name));
const is_state = is_state_source(binding, context.state.analysis);
const is_proxy = should_proxy(value, context.state.scope);
const is_proxy = should_proxy(visited, context.state.scope);
const compose = [];
if (parallelize) {
if (rune === '$state' && is_proxy) {
compose.push(b.id('$.proxy'));
if (dev && !is_state) {
compose.push(
b.arrow([b.id('proxy')], b.call('$.tag_proxy', b.id('proxy'), b.literal(id.name)))
);
}
}
if (rune === '$state' && is_proxy) {
value = b.call('$.proxy', value);
if (is_state) {
compose.push(b.id('$.state'));
if (dev) {
compose.push(
b.arrow([b.id('source')], b.call('$.tag', b.id('source'), b.literal(id.name)))
);
}
}
return b.call(
'$.async_compose',
/** @type {Expression} */ (
context.visit(/** @type {AwaitExpression} */ (value).argument)
),
...compose
);
} else {
let value = visited;
if (rune === '$state' && is_proxy) {
value = b.call('$.proxy', value);
if (dev && !is_state) {
value = b.call('$.tag_proxy', value, b.literal(id.name));
if (dev && !is_state) {
value = b.call('$.tag_proxy', value, b.literal(id.name));
}
}
}
if (is_state) {
value = b.call('$.state', value);
if (is_state) {
value = b.call('$.state', value);
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
}
}
return value;
}
return value;
};
if (declarator.id.type === 'Identifier') {
const expression = /** @type {Expression} */ (context.visit(value));
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, expression))
state_declarators.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, expression, value))
);
} else {
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
state_declarators.push(
b.declarator(tmp, /** @type {Expression} */ (context.visit(value))),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
@ -186,12 +277,36 @@ export function VariableDeclaration(node, context) {
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, value)
? create_state_declarator(binding.node, value, path.expression)
: value
);
})
);
}
if (!parallelize) {
declarations.push(...state_declarators);
} else {
const declarators = state_declarators.map(({ id, init }) => ({
id,
init: /** @type {Expression} */ (init)
}));
if (current_chunk && (current_chunk.kind === node.kind || current_chunk.kind === null)) {
current_chunk.declarators.push(...declarators);
current_chunk.bindings.push(...bindings);
current_chunk.position = position;
current_chunk.kind = node.kind;
} else {
/** @type {ParallelizedChunk} */
const chunk = {
kind: node.kind,
declarators,
position,
bindings
};
context.state.current_parallelized_chunk = chunk;
context.state.parallelized_chunks.push(chunk);
}
}
continue;
}
@ -200,6 +315,25 @@ export function VariableDeclaration(node, context) {
const is_async = context.state.analysis.async_deriveds.has(
/** @type {CallExpression} */ (init)
);
let parallelize = false;
const current_chunk = context.state.current_parallelized_chunk;
if (
is_async &&
context.state.analysis.instance &&
context.state.scope === context.state.analysis.instance.scope &&
// TODO make it work without this
declarator.id.type === 'Identifier'
) {
parallelize = can_be_parallelized(value, context.state.scope, context.state.analysis, [
...(current_chunk?.bindings ?? []),
...context.state.scope.get_bindings(declarator)
]);
}
/** @type {VariableDeclarator[]} */
const derived_declarators = [];
/** @type {Binding[]} */
const bindings = [];
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (
@ -217,17 +351,22 @@ export function VariableDeclaration(node, context) {
location ? b.literal(location) : undefined
);
call = b.call(b.await(b.call('$.save', call)));
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
if (!parallelize) call = b.call(b.await(b.call('$.save', call)));
if (dev) {
call = b.call(
'$.tag' + (parallelize ? '_async' : ''),
call,
b.literal(declarator.id.name)
);
}
bindings.push(/** @type {Binding} */ (context.state.scope.get(declarator.id.name)));
derived_declarators.push(b.declarator(declarator.id, call));
} else {
if (rune === '$derived') expression = b.thunk(expression);
let call = b.call('$.derived', expression);
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 {
const init = /** @type {CallExpression} */ (declarator.init);
@ -260,8 +399,7 @@ export function VariableDeclaration(node, context) {
const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
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);
@ -277,14 +415,13 @@ export function VariableDeclaration(node, context) {
const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
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) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
const call = b.call('$.derived', b.thunk(expression));
declarations.push(
derived_declarators.push(
b.declarator(
path.node,
dev
@ -295,12 +432,37 @@ export function VariableDeclaration(node, context) {
}
}
if (!parallelize) {
declarations.push(...derived_declarators);
} else if (derived_declarators.length > 0) {
const declarators = derived_declarators.map(({ id, init }) => ({
id,
init: /** @type {Expression} */ (init)
}));
if (current_chunk && (current_chunk.kind === node.kind || current_chunk.kind === null)) {
current_chunk.declarators.push(...declarators);
current_chunk.bindings.push(...bindings);
current_chunk.position = position;
current_chunk.kind = node.kind;
} else {
/** @type {ParallelizedChunk} */
const chunk = {
kind: node.kind,
declarators,
position,
bindings
};
context.state.current_parallelized_chunk = chunk;
context.state.parallelized_chunks.push(chunk);
}
}
continue;
}
}
} else {
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (context.state.scope.get_bindings(declarator));
const bindings = context.state.scope.get_bindings(declarator);
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
@ -403,13 +565,9 @@ export function VariableDeclaration(node, context) {
* @param {Expression} value
*/
function create_state_declarators(declarator, context, value) {
const immutable = context.state.analysis.immutable ? b.true : undefined;
if (declarator.id.type === 'Identifier') {
return [
b.declarator(
declarator.id,
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
)
];
return [b.declarator(declarator.id, b.call('$.mutable_source', value, immutable))];
}
const tmp = b.id(context.state.scope.generate('tmp'));
@ -424,15 +582,13 @@ function create_state_declarators(declarator, context, value) {
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
...paths.map(({ expression, node }) => {
const value = /** @type {Expression} */ (context.visit(expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (node).name);
return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
: value
node,
binding?.kind === 'state' ? b.call('$.mutable_source', value, immutable) : value
);
})
];

@ -184,6 +184,17 @@ export function tag(source, label) {
return source;
}
/**
* @template T
* @param {Promise<Value<T>>} promise
* @param {string} label
* @returns {Promise<Value<T>>}
*/
export async function tag_async(promise, label) {
const source = await promise;
return tag(source, label);
}
/**
* @param {unknown} value
* @param {string} label

@ -7,7 +7,7 @@ export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace, tag, tag_proxy } from './dev/tracing.js';
export { trace, tag, tag_async, tag_proxy } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
export { async } from './dom/blocks/async.js';
export { validate_snippet_args } from './dev/validation.js';
@ -99,7 +99,9 @@ export {
with_script
} from './dom/template.js';
export {
all,
async_body,
async_compose,
for_await_track_reactivity_loss,
save,
track_reactivity_loss

@ -191,3 +191,28 @@ export async function async_body(fn) {
unsuspend();
}
}
/**
* @template T
* @param {Array<Promise<T>>} promises
* @returns {Promise<Array<T>>}
*/
export function all(...promises) {
return Promise.all(
promises.map((promise) =>
promise instanceof Promise ? save(promise).then((restore) => restore()) : promise
)
);
}
/**
* @param {Promise<any>} promise
* @param {Array<(arg: any) => any>} fns
*/
export async function async_compose(promise, ...fns) {
let res = await promise;
for (const fn of fns) {
res = fn(res);
}
return res;
}

Loading…
Cancel
Save