Skip to content

Commit

Permalink
perf(reactivity): use bitwise dep markers to optimize re-tracking (#4017
Browse files Browse the repository at this point in the history
)
  • Loading branch information
basvanmeurs authored Jul 7, 2021
1 parent 05e5c98 commit a889895
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 49 deletions.
93 changes: 92 additions & 1 deletion packages/reactivity/__tests__/effect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
DebuggerEvent,
markRaw,
shallowReactive,
readonly
readonly,
ReactiveEffectRunner
} from '../src/index'
import { ITERATE_KEY } from '../src/effect'

Expand Down Expand Up @@ -490,6 +491,96 @@ describe('reactivity/effect', () => {
expect(conditionalSpy).toHaveBeenCalledTimes(2)
})

it('should handle deep effect recursion using cleanup fallback', () => {
const results = reactive([0])
const effects: { fx: ReactiveEffectRunner; index: number }[] = []
for (let i = 1; i < 40; i++) {
;(index => {
const fx = effect(() => {
results[index] = results[index - 1] * 2
})
effects.push({ fx, index })
})(i)
}

expect(results[39]).toBe(0)
results[0] = 1
expect(results[39]).toBe(Math.pow(2, 39))
})

it('should register deps independently during effect recursion', () => {
const input = reactive({ a: 1, b: 2, c: 0 })
const output = reactive({ fx1: 0, fx2: 0 })

const fx1Spy = jest.fn(() => {
let result = 0
if (input.c < 2) result += input.a
if (input.c > 1) result += input.b
output.fx1 = result
})

const fx1 = effect(fx1Spy)

const fx2Spy = jest.fn(() => {
let result = 0
if (input.c > 1) result += input.a
if (input.c < 3) result += input.b
output.fx2 = result + output.fx1
})

const fx2 = effect(fx2Spy)

expect(fx1).not.toBeNull()
expect(fx2).not.toBeNull()

expect(output.fx1).toBe(1)
expect(output.fx2).toBe(2 + 1)
expect(fx1Spy).toHaveBeenCalledTimes(1)
expect(fx2Spy).toHaveBeenCalledTimes(1)

fx1Spy.mockClear()
fx2Spy.mockClear()
input.b = 3
expect(output.fx1).toBe(1)
expect(output.fx2).toBe(3 + 1)
expect(fx1Spy).toHaveBeenCalledTimes(0)
expect(fx2Spy).toHaveBeenCalledTimes(1)

fx1Spy.mockClear()
fx2Spy.mockClear()
input.c = 1
expect(output.fx1).toBe(1)
expect(output.fx2).toBe(3 + 1)
expect(fx1Spy).toHaveBeenCalledTimes(1)
expect(fx2Spy).toHaveBeenCalledTimes(1)

fx1Spy.mockClear()
fx2Spy.mockClear()
input.c = 2
expect(output.fx1).toBe(3)
expect(output.fx2).toBe(1 + 3 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(1)

// Invoked twice due to change of fx1.
expect(fx2Spy).toHaveBeenCalledTimes(2)

fx1Spy.mockClear()
fx2Spy.mockClear()
input.c = 3
expect(output.fx1).toBe(3)
expect(output.fx2).toBe(1 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(1)
expect(fx2Spy).toHaveBeenCalledTimes(1)

fx1Spy.mockClear()
fx2Spy.mockClear()
input.a = 10
expect(output.fx1).toBe(3)
expect(output.fx2).toBe(10 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(0)
expect(fx2Spy).toHaveBeenCalledTimes(1)
})

it('should not double wrap if the passed function is a effect', () => {
const runner = effect(() => {})
const otherRunner = effect(runner)
Expand Down
51 changes: 51 additions & 0 deletions packages/reactivity/src/Dep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ReactiveEffect, getTrackOpBit } from './effect'

export type Dep = Set<ReactiveEffect> & TrackedMarkers

/**
* wasTracked and newTracked maintain the status for several levels of effect
* tracking recursion. One bit per level is used to define wheter the dependency
* was/is tracked.
*/
type TrackedMarkers = { wasTracked: number; newTracked: number }

export function createDep(effects?: ReactiveEffect[]): Dep {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.wasTracked = 0
dep.newTracked = 0
return dep
}

export function wasTracked(dep: Dep): boolean {
return hasBit(dep.wasTracked, getTrackOpBit())
}

export function newTracked(dep: Dep): boolean {
return hasBit(dep.newTracked, getTrackOpBit())
}

export function setWasTracked(dep: Dep) {
dep.wasTracked = setBit(dep.wasTracked, getTrackOpBit())
}

export function setNewTracked(dep: Dep) {
dep.newTracked = setBit(dep.newTracked, getTrackOpBit())
}

export function resetTracked(dep: Dep) {
const trackOpBit = getTrackOpBit()
dep.wasTracked = clearBit(dep.wasTracked, trackOpBit)
dep.newTracked = clearBit(dep.newTracked, trackOpBit)
}

function hasBit(value: number, bit: number): boolean {
return (value & bit) > 0
}

function setBit(value: number, bit: number): number {
return value | bit
}

function clearBit(value: number, bit: number): number {
return value & ~bit
}
3 changes: 2 additions & 1 deletion packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref'
import { isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive'
import { Dep } from './Dep'

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T
Expand Down Expand Up @@ -30,7 +31,7 @@ export const setComputedScheduler = (s: ComputedScheduler | undefined) => {
}

class ComputedRefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined
public dep?: Dep = undefined

private _value!: T
private _dirty = true
Expand Down
90 changes: 81 additions & 9 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
import { EffectScope, recordEffectScope } from './effectScope'
import {
createDep,
Dep,
newTracked,
resetTracked,
setNewTracked,
setWasTracked,
wasTracked
} from './Dep'

// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

Expand Down Expand Up @@ -56,19 +64,57 @@ export class ReactiveEffect<T = any> {
return this.fn()
}
if (!effectStack.includes(this)) {
this.cleanup()
try {
enableTracking()
effectStack.push((activeEffect = this))
enableTracking()

effectTrackDepth++

if (effectTrackDepth <= maxMarkerBits) {
this.initDepMarkers()
} else {
this.cleanup()
}
return this.fn()
} finally {
effectStack.pop()
if (effectTrackDepth <= maxMarkerBits) {
this.finalizeDepMarkers()
}
effectTrackDepth--
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}

initDepMarkers() {
const { deps } = this
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
setWasTracked(deps[i])
}
}
}

finalizeDepMarkers() {
const { deps } = this
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(this)
} else {
deps[ptr++] = dep
}
resetTracked(dep)
}
deps.length = ptr
}
}

cleanup() {
const { deps } = this
if (deps.length) {
Expand All @@ -90,6 +136,20 @@ export class ReactiveEffect<T = any> {
}
}

// The number of effects currently being tracked recursively.
let effectTrackDepth = 0

/**
* The bitwise track markers support at most 30 levels op recursion.
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
* When recursion depth is greater, fall back to using a full cleanup.
*/
const maxMarkerBits = 30

export function getTrackOpBit(): number {
return 1 << effectTrackDepth
}

export interface ReactiveEffectOptions {
lazy?: boolean
scheduler?: EffectScheduler
Expand Down Expand Up @@ -158,7 +218,8 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
dep = createDep()
depsMap.set(key, dep)
}

const eventInfo = __DEV__
Expand All @@ -173,10 +234,21 @@ export function isTracking() {
}

export function trackEffects(
dep: Set<ReactiveEffect>,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (!dep.has(activeEffect!)) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
setNewTracked(dep)
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}

if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
Expand Down Expand Up @@ -267,7 +339,7 @@ export function trigger(
effects.push(...dep)
}
}
triggerEffects(new Set(effects), eventInfo)
triggerEffects(createDep(effects), eventInfo)
}
}

Expand Down
18 changes: 7 additions & 11 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {
isTracking,
ReactiveEffect,
trackEffects,
triggerEffects
} from './effect'
import { isTracking, trackEffects, triggerEffects } from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { isArray, isObject, hasChanged } from '@vue/shared'
import { reactive, isProxy, toRaw, isReactive } from './reactive'
import { CollectionTypes } from './collectionHandlers'
import { createDep, Dep } from './Dep'

export declare const RefSymbol: unique symbol

Expand All @@ -27,19 +23,19 @@ export interface Ref<T = any> {
/**
* Deps are maintained locally rather than in depsMap for performance reasons.
*/
dep?: Set<ReactiveEffect>
dep?: Dep
}

type RefBase<T> = {
dep?: Set<ReactiveEffect>
dep?: Dep
value: T
}

export function trackRefValue(ref: RefBase<any>) {
if (isTracking()) {
ref = toRaw(ref)
if (!ref.dep) {
ref.dep = new Set<ReactiveEffect>()
ref.dep = createDep()
}
if (__DEV__) {
trackEffects(ref.dep, {
Expand Down Expand Up @@ -101,7 +97,7 @@ export function shallowRef(value?: unknown) {
}

class RefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined
public dep?: Dep = undefined

private _value: T

Expand Down Expand Up @@ -170,7 +166,7 @@ export type CustomRefFactory<T> = (
}

class CustomRefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined
public dep?: Dep = undefined

private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
Expand Down
Loading

0 comments on commit a889895

Please sign in to comment.