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

feat(reactivity): bitwise dep markers to optimize re-tracking #4017

Merged
merged 1 commit into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -31,7 +32,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