Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memory leak fix #1085

Merged
merged 24 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9e6af3b
chore: create memory leak example
shining-mind Dec 22, 2023
953f023
fix: fixed memory leaks
kobezzza Jan 12, 2024
584e3f2
chore: updated dependencies
kobezzza Jan 12, 2024
b1817a0
fix: fixed memory leak
kobezzza Jan 16, 2024
cd128ee
chore(ts): no implicit any
kobezzza Jan 16, 2024
9feb315
fix: fixed memory leak
kobezzza Jan 16, 2024
f03c6a1
feat: now `group` can be provided as a function
kobezzza Jan 17, 2024
54f06c6
fix: fixed memory leaks
kobezzza Jan 17, 2024
88ea685
fix: added memoization for handlers
kobezzza Jan 17, 2024
3ac9447
fix: clear only functional vnodes
kobezzza Jan 17, 2024
22cc4bf
chore: merged with origin/v4
kobezzza Jan 17, 2024
f8e4c95
chore: fixed TS
kobezzza Jan 17, 2024
759281b
chore: removed code for debugging
kobezzza Jan 17, 2024
4b2450d
chore: fixed ESLint warnings
kobezzza Jan 17, 2024
74bbd8f
fix: prefer tmp insteadof self
kobezzza Jan 17, 2024
4f2af69
fix: fixed off without providing a callback
kobezzza Jan 17, 2024
6ef8f7e
fix: fixed specs
kobezzza Jan 17, 2024
35a0272
fix: preserve some meta after removal
kobezzza Jan 17, 2024
118dd56
Update src/core/component/watch/create.ts
kobezzza Jan 17, 2024
8a13878
Update src/core/component/watch/create.ts
kobezzza Jan 17, 2024
9d5e6a2
chore: fixes for review
kobezzza Jan 17, 2024
697fb62
Merge branch 'memory-leak-fix' of https://github.com/V4Fire/Client in…
kobezzza Jan 17, 2024
517ee36
Merge remote-tracking branch 'origin/v4' into memory-leak-fix
kobezzza Jan 17, 2024
8fedae4
chore: :up: 4.0.0-beta.49
kobezzza Jan 17, 2024
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ Changelog

_Note: Gaps between patch versions are faulty, broken or test releases._

## v4.0.0-beta.49 (2024-01-17)

#### :rocket: New Feature

* Now the `render` method can accept the name of an asynchronous group to control the invocation of destructors `components/friends/vdom`

#### :bug: Bug Fix

* Fixed memory leaks when switching pages `bDynamicPage`
* Fixed a memory leak when creating dynamic components via the VDOM API `core/component/engines/vue3`
* Fixed memory leaks when removing components `core/component/init`
* Added memoization for the `getParent` and `getRoot` props to prevent unnecessary re-renders `build/snakeskin`

## v4.0.0-beta.48 (2024-01-17)

#### :boom: Breaking Change
Expand Down
6 changes: 6 additions & 0 deletions build/snakeskin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Changelog
> - :house: [Internal]
> - :nail_care: [Polish]

## v4.0.0-beta.49 (2024-01-17)

#### :bug: Bug Fix

* Added memoization for the `getParent` and `getRoot` props to prevent unnecessary re-renders

## v4.0.0-beta.38 (2023-11-15)

#### :bug: Bug Fix
Expand Down
4 changes: 2 additions & 2 deletions build/snakeskin/default-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ function tagFilter({name, attrs = {}}, tplName, cursor) {
attrs[':componentIdProp'] = [`componentId + ${JSON.stringify(id)}`];
}

attrs[':getRoot'] = ["() => ('getRoot' in self ? self.getRoot?.() : null) ?? self.$root"];
attrs[':getParent'] = ["() => typeof $restArgs !== 'undefined' ? $restArgs?.ctx ?? self : self"];
attrs[':getRoot'] = ["self.tmp.__getRoot__ ??= () => ('getRoot' in self ? self.getRoot?.() : null) ?? self.$root"];
attrs[':getParent'] = ["self.tmp.__getParent__ ??= () => typeof $restArgs !== 'undefined' ? $restArgs?.ctx ?? self : self"];

if (component.inheritMods !== false && !attrs[':modsProp']) {
attrs[':modsProp'] = ['sharedMods'];
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"packageManager": "[email protected]",
"typings": "index.d.ts",
"license": "MIT",
"version": "4.0.0-beta.48",
"version": "4.0.0-beta.49",
"author": {
"name": "kobezzza",
"email": "[email protected]",
Expand Down
6 changes: 6 additions & 0 deletions src/components/base/b-dynamic-page/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Changelog
> - :house: [Internal]
> - :nail_care: [Polish]

## v4.0.0-beta.49 (2024-01-17)

#### :bug: Bug Fix

* Fixed memory leaks when switching pages

## v4.0.0-alpha.1 (2022-12-14)

#### :boom: Breaking Change
Expand Down
3 changes: 2 additions & 1 deletion src/components/base/b-dynamic-page/b-dynamic-page.ss
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
? delete attrs[':keepAlive']
? delete attrs[':dispatching']

< template v-for = el in asyncRender.iterate(renderIterator, {filter: renderFilter, group: registerRenderingGroup()})
< template v-for = el in asyncRender.iterate(renderIterator, {filter: renderFilter, group: registerRenderGroup})
< component.&__component &
v-if = !pageTakenFromCache |
ref = component |
Expand All @@ -32,5 +32,6 @@
:dispatching = true |
:renderComponentId = true |

v-attrs = {'@hook:destroyed': createPageDestructor()} |
${attrs}
.
116 changes: 70 additions & 46 deletions src/components/base/b-dynamic-page/b-dynamic-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,25 +95,24 @@ export default class bDynamicPage extends iDynamicPage {
readonly pageGetter!: PageGetter;

/**
* If true, then when moving from one page to another, the old page is saved in the cache under its own name.
* When you return to this page, it will be restored. This helps to optimize switching between pages, but increases
* memory consumption.
* If set to true, the previous pages will be cached under their own names,
* allowing them to be restored when revisited.
* This optimization helps improve page switching but may increase memory usage.
*
* Note that when a page is switched, it will be deactivated by calling `deactivate`.
* When the page is restored, it will be activated by calling `activate`.
* Please note that when a page is switched, it will be deactivated through the `deactivate` function.
* Similarly, when the page is restored, it will be activated using the `activate` function.
*/
@prop(Boolean)
readonly keepAlive: boolean = false;

/**
* The maximum number of pages in the `keepAlive` global cache
* The maximum number of pages that can be stored in the global cache of `keepAlive`
*/
@prop(Number)
readonly keepAliveSize: number = 10;

/**
* A dictionary of `keepAlive` caches.
* The keys represent cache groups (the default is `global`).
* A dictionary of `keepAlive` caches, where the keys represent cache groups (with the default being `global`)
*/
@system<bDynamicPage>((o) => o.sync.link('keepAliveSize', (size: number) => ({
...o.keepAliveCache,
Expand All @@ -127,15 +126,17 @@ export default class bDynamicPage extends iDynamicPage {
keepAliveCache!: Dictionary<AbstractCache<iDynamicPageEl>>;

/**
* A predicate to include pages in `keepAlive` caching: if not specified, all loaded pages will be cached.
* It can be defined as:
* A predicate to determine which pages should be included in `keepAlive` caching.
* If not specified, all loaded pages will be cached.
*
* 1. a component name (or a list of names);
* 2. a regular expression;
* 3. a function that takes a component name and returns:
* * `true` (include), `false` (does not include);
* * a string key for caching (used instead of the component name);
* * or a special object with information about the caching strategy being used.
* The predicate can be defined in three ways:
* 1. As a component name or a list of component names.
* 2. As a regular expression.
* 3. As a function that takes a component name and returns one of the following:
* - `true` (to include the page in caching).
* - `false` (to exclude the page from caching).
* - A string key to be used for caching instead of the component name.
* - A special object with information about the caching strategy being used.
*/
@prop({
type: [String, Array, RegExp, Function],
Expand All @@ -145,9 +146,11 @@ export default class bDynamicPage extends iDynamicPage {
readonly include?: Include;

/**
* A predicate to exclude some pages from `keepAlive` caching.
* It can be defined as a component name (or a list of names), regular expression,
* or a function that takes a component name and returns `true` (exclude) or `false` (does not exclude).
* A predicate to exclude certain pages from `keepAlive` caching can be defined in three ways:
* 1. As a component name or a list of component names.
* 2. As a regular expression.
* 3. As a function that takes a component name and returns `true` to exclude the page from caching,
* or `false` to include the page in caching.
*/
@prop({
type: [String, Array, RegExp, Function],
Expand All @@ -163,7 +166,7 @@ export default class bDynamicPage extends iDynamicPage {
readonly emitter?: EventEmitterLike;

/**
* Page switching event name
* The page switching event name
*/
@prop({
type: String,
Expand All @@ -179,22 +182,25 @@ export default class bDynamicPage extends iDynamicPage {
@computed({cache: false, dependencies: ['page']})
get component(): CanPromise<iDynamicPage> {
const
c = this.$refs.component;
that = this,
componentRef = this.$refs.component;

const getComponent = () => {
if (componentRef != null && (!Object.isArray(componentRef) || componentRef.length > 0)) {
return getComponent();
}

return this.waitRef('component').then(getComponent);

function getComponent() {
const
c = this.$refs.component!;
componentRef = that.$refs.component!;

if (Object.isArray(c)) {
return c[0];
if (Object.isArray(componentRef)) {
return componentRef[0];
}

return c;
};

return c != null && (!Object.isArray(c) || c.length > 0) ?
getComponent() :
this.waitRef('component').then(getComponent);
return componentRef;
}
}

override get unsafe(): UnsafeGetter<UnsafeBDynamicPage<this>> {
Expand Down Expand Up @@ -230,10 +236,17 @@ export default class bDynamicPage extends iDynamicPage {
* Registered groups of asynchronous render tasks
*/
@system()
protected renderingGroups: Set<string> = new Set();
protected renderGroups: Set<string> = new Set();

/**
* The name of the current rendering group
*/
protected get currentRenderGroup(): string {
return `pageRendering-${this.renderCounter}`;
}

/**
* Render loop iterator (used with `asyncRender`)
* The render loop iterator for `asyncRender`
*/
protected get renderIterator(): CanPromise<number> {
if (SSR) {
Expand Down Expand Up @@ -289,14 +302,28 @@ export default class bDynamicPage extends iDynamicPage {
/**
* Registers a new group for asynchronous rendering and returns it
*/
protected registerRenderingGroup(): string {
const group = `pageRendering-${this.renderCounter++}`;
this.renderingGroups.add(group);
return group;
protected registerRenderGroup(): string {
this.renderCounter++;
this.renderGroups.add(this.currentRenderGroup);
return this.currentRenderGroup;
}

/**
* Render loop filter (used with `asyncRender`)
* Creates a page destructor function
*/
protected createPageDestructor(): Function {
const
group = this.currentRenderGroup,
groupRgxp = new RegExp(RegExp.escape(group));

return () => {
this.async.clearAll({group: groupRgxp});
this.renderGroups.delete(group);
};
}

/**
* The render loop filter for `asyncRender`
*/
protected renderFilter(): CanPromise<boolean> {
const canPass =
Expand All @@ -310,20 +337,17 @@ export default class bDynamicPage extends iDynamicPage {
return true;
}

const
{unsafe, route} = this;
const {
unsafe,
route
} = this;

return new SyncPromise((resolve) => {
[...this.renderingGroups].slice(0, -2).forEach((group) => {
this.async.clearAll({group: new RegExp(RegExp.escape(group))});
this.renderingGroups.delete(group);
});

this.onPageChange = onPageChange(resolve, this.route);
});

function onPageChange(
resolve: Function,
resolve: (status: boolean) => void,
currentRoute: typeof route
): AnyFunction {
return (newPage: CanUndef<string>, currentPage: CanUndef<string>) => {
Expand Down
1 change: 1 addition & 0 deletions src/components/friends/async-render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ This can optimize the browser rendering process.

A group name for manually clearing pending tasks via the `async` module.
Providing this value disables the automatic cleanup of render tasks on the `update` hook.
If this parameter is set as a function, the group name will be dynamically calculated on each iteration.

```
< .container v-async-target
Expand Down
6 changes: 3 additions & 3 deletions src/components/friends/async-render/helpers/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function addRenderTask(
): Promise<void> {
const
$a = this.async,
group = opts.group ?? 'asyncComponents';
group = (Object.isFunction(opts.group) ? opts.group() : opts.group) ?? 'asyncComponents';

return new SyncPromise((resolve, reject) => {
const taskDesc = {
Expand Down Expand Up @@ -70,8 +70,6 @@ export function addRenderTask(
* @param [childComponentEls] - root elements of the child components
*/
export function destroyNode(this: Friend, node: Node, childComponentEls: Element[] = []): void {
node.parentNode?.removeChild(node);

childComponentEls.forEach((child) => {
try {
(<ComponentElement<iBlock>>child).component?.unsafe.$destroy();
Expand All @@ -87,4 +85,6 @@ export function destroyNode(this: Friend, node: Node, childComponentEls: Element
} catch (err) {
stderr(err);
}

node.parentNode?.removeChild(node);
}
3 changes: 2 additions & 1 deletion src/components/friends/async-render/interface/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface TaskOptions<El = unknown, D = unknown> {
/**
* A group name to manual clearing of pending tasks via the [[Async]] module.
* Providing this value disables automatically cleanup of render tasks on the `update` hook.
* If this parameter is set as a function, the group name will be dynamically calculated on each iteration.
*
* @example
* ```
Expand All @@ -45,7 +46,7 @@ export interface TaskOptions<El = unknown, D = unknown> {
* Cancel rendering
* ```
*/
group?: string;
group?: string | (() => string);

/**
* A function to filter elements to render.
Expand Down
4 changes: 2 additions & 2 deletions src/components/friends/async-render/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function iterate(
// eslint-disable-next-line no-constant-condition
rendering: while (true) {
if (opts.group != null) {
group = `asyncComponents:${opts.group}:${chunkI}`;
group = `asyncComponents:${Object.isFunction(opts.group) ? opts.group() : opts.group}:${chunkI}`;
}

let
Expand Down Expand Up @@ -334,7 +334,7 @@ export function iterate(
renderedVnode = Object.cast(vnode.el);

} else {
renderedVnode = render.call(that, Object.cast(vnode));
renderedVnode = render.call(that, Object.cast(vnode), group);
}

const
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,12 @@

< template v-if = stage === 'updating the parent component state'
< .&__result v-async-target
{{ void(tmp.oldRefs = $refs.btn) }}

< template v-for = el in asyncRender.iterate(2)
< b-button ref = btn | v-func = false
< template #default = {ctx}
Element: {{ el }}; Hook: {{ ctx.hook }};

< button.&__update @click = $forceUpdate
< button.&__update @click = tmp.oldRefs=$refs.btn.slice(), $forceUpdate()
Update state

< template v-if = stage === 'clearing by the specified group name'
Expand Down
3 changes: 1 addition & 2 deletions src/components/friends/async-render/test/unit/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,7 @@ test.describe('friends/async-render', () => {

const hooks = await target.evaluate((ctx) => {
const oldRefs = <ComponentInterface[]>ctx.unsafe.tmp.oldRefs;

return [oldRefs[0].hook, oldRefs[1].hook];
return [oldRefs[0].hook, oldRefs[1]?.hook];
});

test.expect(hooks).toEqual([
Expand Down
6 changes: 6 additions & 0 deletions src/components/friends/vdom/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Changelog
> - :house: [Internal]
> - :nail_care: [Polish]

## v4.0.0-beta.49 (2024-01-17)

#### :rocket: New Feature

* Now the `render` method can accept the name of an asynchronous group to control the invocation of destructors

## v4.0.0-alpha.1 (2022-12-14)

#### :boom: Breaking Change
Expand Down
Loading
Loading