fix(reactivity): remove used bit

This commit is contained in:
Hoikan 2024-05-17 17:57:07 +08:00
parent 3665d927ae
commit 5fc41b8e5e
14 changed files with 271 additions and 139 deletions

View File

@ -51,11 +51,11 @@ export function functionalMacroAnalyze(): Visitor {
// watch
if (calleeName === WATCH) {
const fnNode = extractFnFromMacro(expression, WATCH);
const deps = getWatchDeps(expression);
const depsPath = getWatchDeps(expression);
const depMask = getDependenciesFromNode(deps ?? fnNode, ctx);
const [deps, depMask] = getDependenciesFromNode(depsPath ?? fnNode, ctx);
addWatch(ctx.current, fnNode, depMask);
addWatch(ctx.current, fnNode, deps, depMask);
return;
}
}

View File

@ -1,5 +1,5 @@
import { type NodePath } from '@babel/core';
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
import { AnalyzeContext, Analyzer, Bitmap, ComponentNode, Visitor } from './types';
import { addLifecycle, createComponentNode } from './nodeFactory';
import { variablesAnalyze } from './variablesAnalyze';
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
@ -7,6 +7,9 @@ import { getFnBodyPath } from '../utils';
import { viewAnalyze } from './viewAnalyze';
import { WILL_MOUNT } from '../constants';
import { types as t } from '@openinula/babel-api';
import { ViewParticle } from '@openinula/reactivity-parser';
import { pruneComponentUnusedBit } from './pruneComponentUnusedBit';
const builtinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
function mergeVisitor(...visitors: Analyzer[]): Visitor {
@ -73,6 +76,7 @@ export function analyzeFnComp(
addLifecycle(componentNode, WILL_MOUNT, t.blockStatement(context.unhandledNode));
}
}
/**
* The process of analyzing the component
* 1. identify the component
@ -94,5 +98,7 @@ export function analyze(
const root = createComponentNode(fnName, path);
analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags });
pruneComponentUnusedBit(root);
return root;
}

View File

@ -29,6 +29,8 @@ export function createComponentNode(
name,
children: undefined,
variables: [],
usedBit: 0,
usedPropertySet: parent ? new Set(parent.usedPropertySet) : new Set<string>(),
_reactiveBitMap: parent ? new Map<string, number>(parent._reactiveBitMap) : new Map<string, number>(),
lifecycle: {},
parent,
@ -52,6 +54,9 @@ export function addProperty(comp: ComponentNode, name: string, value: t.Expressi
const bit = 1 << idx;
const bitmap = depBits ? depBits | bit : bit;
if (depBits) {
comp.usedBit |= depBits;
}
comp._reactiveBitMap.set(name, bitmap);
comp.variables.push({ name, value, isComputed: !!depBits, type: 'reactive', depMask: bitmap, level: comp.level });
}
@ -61,6 +66,7 @@ export function addMethod(comp: ComponentNode, name: string, value: FunctionalEx
}
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) {
comp.usedBit |= subComp.usedBit;
comp.variables.push({ ...subComp, type: 'subComp' });
}
@ -75,17 +81,21 @@ export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t
export function addWatch(
comp: ComponentNode,
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
depMask: Bitmap
deps: Set<string>,
usedBit: Bitmap
) {
// if watch not exist, create a new one
if (!comp.watch) {
comp.watch = [];
}
comp.watch.push({ callback, depMask });
comp.usedPropertySet = new Set([...comp.usedPropertySet, ...deps]);
comp.usedBit |= usedBit;
comp.watch.push({ callback });
}
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>) {
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>, usedBit: Bitmap) {
// TODO: Maybe we should merge
comp.usedPropertySet = usedPropertySet;
comp.usedBit |= usedBit;
comp.children = view;
}

View File

@ -1,115 +0,0 @@
import { type NodePath } from '@babel/core';
import { AnalyzeContext, Visitor } from './types';
import { PropType } from '../constants';
import { types as t } from '@openinula/babel-api';
export interface Prop {
name: string;
type: PropType;
alias: string | null;
default: t.Expression | null;
nestedProps: string[] | null;
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
}
/**
* Analyze the props deconstructing in the function component
* 1. meet identifier, just collect the name
* 2. has alias, collect the alias name
* 3. has default value, collect the default value
* 4. has rest element, collect the rest element
* 5. nested destructuring, the e2e goal:
* ```js
* function(prop1, prop2: [p20, p21]) {}
* // transform into
* function({ prop1, prop2: [p20, p21] }) {
* let p20, p21
* watch(() => {
* [p20, p21] = prop2
* })
* }
* ```
*/
export function propsAnalyze(): Visitor {
return {
Prop(path: NodePath<t.ObjectProperty | t.RestElement>, ctx) {
if (path.isObjectProperty()) {
// --- normal property ---
const key = path.node.key;
const value = path.node.value;
if (t.isIdentifier(key) || t.isStringLiteral(key)) {
const name = t.isIdentifier(key) ? key.name : key.value;
analyzeSingleProp(value, name, path, ctx);
return;
}
throw Error(`Unsupported key type in object destructuring: ${key.type}`);
} else {
// --- rest element ---
const arg = path.get('argument');
if (!Array.isArray(arg) && arg.isIdentifier()) {
addProp(ctx.current, PropType.REST, arg.node.name);
}
}
},
};
}
function analyzeSingleProp(
value: t.ObjectProperty['value'],
key: string,
path: NodePath<t.ObjectProperty>,
{ current }: AnalyzeContext
) {
let defaultVal: t.Expression | null = null;
let alias: string | null = null;
const nestedProps: string[] | null = [];
let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null;
if (t.isIdentifier(value)) {
// 1. handle alias without default value
// handle alias without default value
if (key !== value.name) {
alias = value.name;
}
} else if (t.isAssignmentPattern(value)) {
// 2. handle default value case
const assignedName = value.left;
defaultVal = value.right;
if (t.isIdentifier(assignedName)) {
if (assignedName.name !== key) {
// handle alias in default value case
alias = assignedName.name;
}
} else {
throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`);
}
} else if (t.isObjectPattern(value) || t.isArrayPattern(value)) {
// 3. nested destructuring
// we should collect the identifier that can be used in the function body as the prop
// e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]}
// we should collect prop1, p20X, p211, p212X
path.get('value').traverse({
Identifier(path) {
// judge if the identifier is a prop
// 1. is the key of the object property and doesn't have alias
// 2. is the item of the array pattern and doesn't have alias
// 3. is alias of the object property
const parentPath = path.parentPath;
if (parentPath.isObjectProperty() && path.parentKey === 'value') {
// collect alias of the object property
nestedProps.push(path.node.name);
} else if (
parentPath.isArrayPattern() ||
parentPath.isObjectPattern() ||
parentPath.isRestElement() ||
(parentPath.isAssignmentPattern() && path.key === 'left')
) {
// collect the key of the object property or the item of the array pattern
nestedProps.push(path.node.name);
}
},
});
nestedRelationship = value;
}
addProp(current, PropType.SINGLE, key, defaultVal, alias, nestedProps, nestedRelationship);
}

View File

@ -0,0 +1,154 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { Bitmap, ComponentNode } from './types';
import { ViewParticle } from '@openinula/reactivity-parser';
/**
* To prune the bitmap of unused properties
* etc.:
* ```js
* let a = 1; // 0b001
* let b = 2; // 0b010 b is not used*, and should be pruned
* let c = 3; // 0b100 -> 0b010(cause bit of b is pruned)
* ```
* @param root
* @param index
*/
export function pruneComponentUnusedBit(comp: ComponentNode<'comp'> | ComponentNode<'subComp'>, index = 1) {
// dfs the component tree
// To store the bitmap of the properties
const bitMap = new Map<string, number>();
const bitPositionToRemove: number[] = [];
comp.variables.forEach(v => {
if (v.type === 'reactive') {
// get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100
const originBit = keepHighestBit(v.depMask);
if ((comp.usedBit & originBit) !== 0) {
v.bit = 1 << index;
bitMap.set(v.name, v.bit);
if (v.isComputed) {
v.depMask = pruneBitmap(v.depMask, bitPositionToRemove);
}
} else {
bitPositionToRemove.push(index);
}
index++;
} else if (v.type === 'subComp') {
pruneComponentUnusedBit(v, index);
}
});
comp.watch?.forEach(watch => {
if (!watch.depMask) {
return;
}
watch.depMask = pruneBitmap(watch.depMask, bitPositionToRemove);
});
// handle children
if (comp.children) {
comp.children.forEach(child => {
if (child.type === 'comp') {
pruneComponentUnusedBit(child as ComponentNode<'comp'>, index);
} else {
pruneViewParticleUnusedBit(child as ViewParticle, bitPositionToRemove);
}
});
}
}
function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) {
// turn the bitmap to binary string
const binary = depMask.toString(2);
// remove the bit
binary
.split('')
.reverse()
.filter((bit, index) => {
return !bitPositionToRemove.includes(index);
})
.reverse()
.join('');
return parseInt(binary, 2);
}
function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove: number[]) {
// dfs the view particle to prune the bitmap
const stack: ViewParticle[] = [particle];
while (stack.length) {
const node = stack.pop()! as ViewParticle;
if (node.type === 'template') {
node.props.forEach(prop => {
prop.depMask = pruneBitmap(prop.depMask, bitPositionToRemove);
});
stack.push(node.template);
} else if (node.type === 'html') {
for (const key in node.props) {
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
}
stack.push(...node.children);
} else if (node.type === 'text') {
node.content.depMask = pruneBitmap(node.content.depMask, bitPositionToRemove);
} else if (node.type === 'for') {
node.array.depMask = pruneBitmap(node.array.depMask, bitPositionToRemove);
stack.push(...node.children);
} else if (node.type === 'if') {
node.branches.forEach(branch => {
branch.condition.depMask = pruneBitmap(branch.condition.depMask, bitPositionToRemove);
stack.push(...branch.children);
});
} else if (node.type === 'env') {
for (const key in node.props) {
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
}
stack.push(...node.children);
} else if (node.type === 'exp') {
node.content.depMask = pruneBitmap(node.content.depMask, bitPositionToRemove);
for (const key in node.props) {
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
}
}
}
}
function keepHighestBit(bitmap: number) {
// 获取二进制数的长度
const length = bitmap.toString(2).length;
// 创建掩码
const mask = 1 << (length - 1);
// 使用按位与运算符只保留最高位
return bitmap & mask;
}
function removeBit(bitmap: number, bitPosition: number) {
// 创建掩码,将目标位右边的位设置为 1,其他位设置为 0
const rightMask = (1 << (bitPosition - 1)) - 1;
// 创建掩码,将目标位左边的位设置为 1,其他位设置为 0
const leftMask = ~rightMask << 1;
// 提取右部分
const rightPart = bitmap & rightMask;
// 提取左部分并右移一位
const leftPart = (bitmap & leftMask) >> 1;
// 组合左部分和右部分
return leftPart | rightPart;
}

View File

@ -34,6 +34,7 @@ export function getDependenciesFromNode(
// ---- Assign deps: count = 1 or count++
let assignDepMask = 0;
const depNodes: Record<string, t.Expression[]> = {};
const deps = new Set<string>();
const visitor = (innerPath: NodePath<t.Identifier>) => {
const propertyKey = innerPath.node.name;
@ -44,6 +45,8 @@ export function getDependenciesFromNode(
assignDepMask |= reactiveBitmap;
} else {
depMask |= reactiveBitmap;
deps.add(propertyKey);
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
depNodes[propertyKey].push(t.cloneNode(innerPath.node));
}
@ -64,7 +67,7 @@ export function getDependenciesFromNode(
// TODO: I think we should throw an error here to indicate the user that there is a loop
}
return depMask;
return [deps, depMask] as const;
}
/**

View File

@ -30,6 +30,7 @@ interface BaseVariable<V> {
export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
type: 'reactive';
level: number;
bit?: Bitmap;
/**
* indicate the dependency of the variable | the index of the reactive variable
* i.e.
@ -69,7 +70,8 @@ export interface ComponentNode<Type = 'comp'> {
/**
* The used properties in the component
*/
usedPropertySet?: Set<string>;
usedPropertySet: Set<string>;
usedBit: Bitmap;
/**
* The map to find the reactive bitmap by name
*/
@ -93,7 +95,7 @@ export interface ComponentNode<Type = 'comp'> {
* The watch fn in the component
*/
watch?: {
depMask: Bitmap;
depMask?: Bitmap;
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
}[];
}

View File

@ -68,7 +68,7 @@ export function variablesAnalyze(): Visitor {
return;
}
depBits = getDependenciesFromNode(init, ctx);
depBits = getDependenciesFromNode(init, ctx)[1];
}
addProperty(ctx.current, id.node.name, init.node || null, depBits);
}

View File

@ -35,14 +35,14 @@ export function viewAnalyze(): Visitor {
parseTemplate: false,
});
// @ts-expect-error TODO: FIX TYPE
const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, {
const [viewParticles, usedPropertySet, usedBit] = parseReactivity(viewUnits, {
babelApi: getBabelApi(),
availableProperties: current.availableVariables,
depMaskMap: current._reactiveBitMap,
reactivityFuncNames,
});
setViewChild(current, viewParticles, usedPropertySet);
setViewChild(current, viewParticles, usedPropertySet, usedBit);
}
},
};

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
import { ComponentNode } from '../../src/analyzer/types';
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
import { genCode, mockAnalyze } from '../mock';
import { describe, expect, it } from 'vitest';
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze, functionalMacroAnalyze]);
describe('prune unused bit', () => {
it('should work', () => {
const root = analyze(/*js*/ `
Component(({}) => {
let name;
let className; // unused
let className1; // unused
let className2; // unused
let count = name; // 1
let doubleCount = count * 2; // 2
const Input = Component(() => {
let count3 = 1;
let count2 = 1;
let count = 1;
return <input>{count}{doubleCount}</input>;
});
return <div className={count}>{doubleCount}</div>;
});
`);
const div = root.children![0] as any;
expect(div.children[0].content.depMask).toEqual(0b111);
expect(div.props.className.depMask).toEqual(0b11);
// @ts-expect-error ignore ts here
const InputCompNode = root.variables[4] as ComponentNode;
// it's the {count}
expect(inputFirstExp.content.depMask).toEqual(0b10000);
// it's the {doubleCount}
expect(inputSecondExp.content.depMask).toEqual(0b1101);
});
});

View File

@ -12,8 +12,8 @@ describe('viewAnalyze', () => {
let name;
let className;
let count = name; // 1
let doubleCount = count* 2; // 2
let doubleCount2 = doubleCount* 2; // 4
let doubleCount = count * 2; // 2
let doubleCount2 = doubleCount * 2; // 4
const Input = Component(() => {
let count = 1;
return <input>{count}{doubleCount}</input>;

View File

@ -29,16 +29,25 @@ export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode
syntaxJSX.default ?? syntaxJSX,
function (api): PluginObj {
register(api);
const seen = new Set();
return {
visitor: {
FunctionExpression: path => {
if (seen.has(path)) {
return;
}
root = analyze('test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags });
seen.add(path);
if (root) {
path.skip();
}
},
ArrowFunctionExpression: path => {
if (seen.has(path)) {
return;
}
root = analyze('test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags });
seen.add(path);
if (root) {
path.skip();
}

View File

@ -8,17 +8,22 @@ import { type ViewParticle, type ReactivityParserConfig } from './types';
* @param config
* @returns [viewParticles, usedProperties]
*/
export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], Set<string>] {
export function parseReactivity(
viewUnits: ViewUnit[],
config: ReactivityParserConfig
): [ViewParticle[], Set<string>, number] {
// ---- ReactivityParser only accepts one view unit at a time,
// so we loop through the view units and get all the used properties
const usedProperties = new Set<string>();
let usedBit = 0;
const dlParticles = viewUnits.map(viewUnit => {
const parser = new ReactivityParser(config);
const dlParticle = parser.parse(viewUnit);
parser.usedProperties.forEach(usedProperties.add.bind(usedProperties));
usedBit |= parser.usedBit;
return dlParticle;
});
return [dlParticles, usedProperties];
return [dlParticles, usedProperties, usedBit];
}
export type * from './types';

View File

@ -54,6 +54,7 @@ export class ReactivityParser {
];
readonly usedProperties = new Set<string>();
usedBit = 0;
/**
* @brief Constructor
@ -328,12 +329,12 @@ export class ReactivityParser {
const keyDep = this.t.isIdentifier(forUnit.key) && forUnit.key.name;
// ---- Generate an identifierDepMap to track identifiers in item and make them reactive
// based on the dependencies from the array
this.config.depMaskMap = new Map([
...this.config.depMaskMap,
...this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])))
.filter(id => !keyDep || id !== keyDep)
.map(id => [id, depMask]),
]);
// this.config.depMaskMap = new Map([
// ...this.config.depMaskMap,
// ...this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])))
// .filter(id => !keyDep || id !== keyDep)
// .map(id => [id, depMask]),
// ]);
const forParticle: ForParticle = {
type: 'for',
@ -346,7 +347,7 @@ export class ReactivityParser {
children: forUnit.children.map(this.parseViewParticle.bind(this)),
key: forUnit.key,
};
this.config.identifierDepMap = prevIdentifierDepMap;
// this.config.identifierDepMap = prevIdentifierDepMap;
return forParticle;
}
@ -507,12 +508,14 @@ export class ReactivityParser {
});
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
this.usedBit |= depMask;
return [depMask, dependencyNodes];
}
/**
* @brief Generate a dependency node from a dependency identifier,
* loop until the parent node is not a binary expression or a member expression
* And turn the member expression into an optional member expression, like info.name -> info?.name
* @param path
* @returns
*/
@ -583,6 +586,7 @@ export class ReactivityParser {
const parsedUnit = parser.parse(viewUnit);
// ---- Collect used properties
parser.usedProperties.forEach(this.usedProperties.add.bind(this.usedProperties));
this.usedBit |= parser.usedBit;
return parsedUnit;
}