createRenderBatcher

Benchmark created on


Setup

const stepsOrder1 = [
    "read", // Read
    "resolveKeyframes", // Write/Read/Write/Read
    "update", // Compute
    "preRender", // Compute
    "render", // Write
    "postRender", // Compute
]

const stepsOrder2 = [
    "read", // Read
    "resolveKeyframes", // Write/Read/Write/Read
    "update", // Compute
    "preRender", // Compute
    "render", // Write
    "postRender", // Compute
]

const stepsOrder3 = [
    "read", // Read
    "resolveKeyframes", // Write/Read/Write/Read
    "update", // Compute
    "preRender", // Compute
    "render", // Write
    "postRender", // Compute
]
const stepsOrderLength = stepsOrder3.length

const createRenderStep = () => {}
const scheduleNextBatch = () => {}
const allowKeepAlive = true

const result = [null]

function createRenderBatcher1(
    scheduleNextBatch,
    allowKeepAlive,
    stepsOrder
) {
    let runNextFrame = false
    let useDefaultElapsed = true

    const state = {
        delta: 0,
        timestamp: 0,
        isProcessing: false,
    }

    const steps = stepsOrder.reduce((acc, key) => {
        acc[key] = createRenderStep(() => (runNextFrame = true))
        return acc
    }, {})

    const processStep = (stepId) => {
        steps[stepId].process(state)
    }

    const processBatch = () => {
        const timestamp = MotionGlobalConfig.useManualTiming
            ? state.timestamp
            : performance.now()
        runNextFrame = false

        state.delta = useDefaultElapsed
            ? 1000 / 60
            : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1)

        state.timestamp = timestamp
        state.isProcessing = true
        stepsOrder.forEach(processStep)
        state.isProcessing = false

        if (runNextFrame && allowKeepAlive) {
            useDefaultElapsed = false
            scheduleNextBatch(processBatch)
        }
    }

    const wake = () => {
        runNextFrame = true
        useDefaultElapsed = true

        if (!state.isProcessing) {
            scheduleNextBatch(processBatch)
        }
    }

    const schedule = stepsOrder.reduce((acc, key) => {
        const step = steps[key]
        acc[key] = (process, keepAlive = false, immediate = false) => {
            if (!runNextFrame) wake()

            return step.schedule(process, keepAlive, immediate)
        }
        return acc
    }, {})

    const cancel = (process) =>
        stepsOrder.forEach((key) => steps[key].cancel(process))

    return { schedule, cancel, state, steps }
}

function createRenderBatcher2(
    scheduleNextBatch,
    allowKeepAlive,
    stepsOrder
) {
    let runNextFrame = false
    let useDefaultElapsed = true

    const state = {
        delta: 0,
        timestamp: 0,
        isProcessing: false,
    }

    const nextFrameFn = () => (runNextFrame = true)
    const steps = {
        read: createRenderStep(nextFrameFn),
        resolveKeyframes: createRenderStep(nextFrameFn),
        update: createRenderStep(nextFrameFn),
        preRender: createRenderStep(nextFrameFn),
        render: createRenderStep(nextFrameFn),
        postRender: createRenderStep(nextFrameFn),
    }

    const processStep = (stepId) => {
        steps[stepId].process(state)
    }

    const processBatch = () => {
        const timestamp = MotionGlobalConfig.useManualTiming
            ? state.timestamp
            : performance.now()
        runNextFrame = false

        state.delta = useDefaultElapsed
            ? 1000 / 60
            : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1)

        state.timestamp = timestamp
        state.isProcessing = true
        for (let i = 0; i < stepsOrderLength; ++i) processStep(stepsOrder[i])
        state.isProcessing = false

        if (runNextFrame && allowKeepAlive) {
            useDefaultElapsed = false
            scheduleNextBatch(processBatch)
        }
    }

    const wake = () => {
        runNextFrame = true
        useDefaultElapsed = true

        if (!state.isProcessing) {
            scheduleNextBatch(processBatch)
        }
    }

    function batchFn(
        process,
        keepAlive = false,
        immediate = false
    ) {
        if (!runNextFrame) wake()

        return this.schedule(process, keepAlive, immediate)
    }
    const schedule = {
        read: batchFn.bind(steps.read),
        resolveKeyframes: batchFn.bind(steps.resolveKeyframes),
        update: batchFn.bind(steps.update),
        preRender: batchFn.bind(steps.preRender),
        render: batchFn.bind(steps.render),
        postRender: batchFn.bind(steps.postRender),
    }

    const cancel = (process) => {
        for (let i = 0; i < stepsOrderLength; ++i)
            steps[stepsOrder[i]].cancel(process)
    }

    return { schedule, cancel, state, steps }
}

function createRenderBatcher3(
    scheduleNextBatch,
    allowKeepAlive,
    stepsOrder
) {
    let runNextFrame = false
    let useDefaultElapsed = true

    const state = {
        delta: 0,
        timestamp: 0,
        isProcessing: false,
    }

    const steps = {
        read: null,
        resolveKeyframes: null,
        update: null,
        preRender: null,
        render: null,
        postRender: null,
    }
    const runNextFrameFn = () => (runNextFrame = true)
    for (let i = 0; i < stepsOrderLength; ++i) {
        const key = stepsOrder[i]
        steps[key] = createRenderStep(runNextFrameFn)
    }

    const processStep = (stepId) => {
        steps[stepId].process(state)
    }

    const processBatch = () => {
        const timestamp = MotionGlobalConfig.useManualTiming
            ? state.timestamp
            : performance.now()
        runNextFrame = false

        state.delta = useDefaultElapsed
            ? 1000 / 60
            : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1)

        state.timestamp = timestamp
        state.isProcessing = true
        for (let i = 0; i < stepsOrderLength; ++i) processStep(stepsOrder[i])
        state.isProcessing = false

        if (runNextFrame && allowKeepAlive) {
            useDefaultElapsed = false
            scheduleNextBatch(processBatch)
        }
    }

    const wake = () => {
        runNextFrame = true
        useDefaultElapsed = true

        if (!state.isProcessing) {
            scheduleNextBatch(processBatch)
        }
    }

    const schedule= {
        read: null,
        resolveKeyframes: null,
        update: null,
        preRender: null,
        render: null,
        postRender: null,
    }
    for (let i = 0; i < stepsOrderLength; ++i) {
        const key = stepsOrder[i]
        const step = steps[key]
        schedule[key] = (
            process,
            keepAlive = false,
            immediate = false
        ) => {
            if (!runNextFrame) wake()

            return step.schedule(process, keepAlive, immediate)
        }
    }

    const cancel = (process) => {
        for (let i = 0; i < stepsOrderLength; ++i)
            steps[stepsOrder[i]].cancel(process)
    }

    return { schedule, cancel, state, steps }
}

Test runner

Ready to run.

Testing in
TestOps/sec
old
result[0] = createRenderBatcher1(scheduleNextBatch, true, stepsOrder1)
ready
new (without for)
result[0] = createRenderBatcher2(scheduleNextBatch, true, stepsOrder2)
ready
new(with for)
result[0] = createRenderBatcher3(scheduleNextBatch, true, stepsOrder3)
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.