parallelize-async-work
ComputerGuy 6 days 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} */
const visitors = {
_: function set_scope(node, { next, state }) {
_: function set_scope(node, { path, next, state }) {
const scope = state.scopes.get(node);
if (scope && scope !== state.scope) {
@ -84,6 +84,9 @@ const visitors = {
} else {
next();
}
if (node.type !== 'VariableDeclaration' && path.at(-1)?.type === 'Program' && state.analysis.instance) {
state.current_parallelized_chunk = null;
}
},
AnimateDirective,
ArrowFunctionExpression,
@ -176,7 +179,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_derived_chunks: [],
current_parallelized_chunk: null
};
const module = /** @type {ESTree.Program} */ (

@ -7,7 +7,7 @@ import type {
AssignmentExpression,
UpdateExpression,
VariableDeclaration,
Declaration
Pattern
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -83,6 +83,18 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
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>;

@ -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,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 {ClientTransformState} 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,20 @@ 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]));
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 { 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 * 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';
@ -200,6 +206,18 @@ export function VariableDeclaration(node, context) {
const is_async = context.state.analysis.async_deriveds.has(
/** @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') {
let expression = /** @type {Expression} */ (
@ -212,7 +230,7 @@ export function VariableDeclaration(node, context) {
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
let call = b.call(
'$.async_derived',
'$.async_derived' + (parallelize ? '_p' : ''),
b.thunk(expression, true),
location ? b.literal(location) : undefined
);
@ -220,14 +238,14 @@ export function VariableDeclaration(node, context) {
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));
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);
@ -253,7 +271,9 @@ export function VariableDeclaration(node, context) {
b.thunk(expression, true),
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) {
@ -261,7 +281,7 @@ export function VariableDeclaration(node, context) {
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);
@ -278,13 +298,13 @@ export function VariableDeclaration(node, context) {
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,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;
}
}

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

@ -189,3 +189,12 @@ 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) => save(promise).then((restore) => restore())));
}

Loading…
Cancel
Save