-
Notifications
You must be signed in to change notification settings - Fork 157
feat(scheduled): add 128-bit BigInt slot collision bitset matrix and power-of-two mask math #1039
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@solid-primitives/scheduled": minor | ||
| --- | ||
|
|
||
| Add `SlotBitset128` branchless 128-bit BigInt scheduling slot collision matrix and `computePowerOfTwoMask` power-of-two ringbuffer allocator. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||||||||||||||||||||||||||||||||||
| export function computePowerOfTwoMask(requestedCapacity: number): { | ||||||||||||||||||||||||||||||||||||||
| capacity: number; | ||||||||||||||||||||||||||||||||||||||
| mask: number; | ||||||||||||||||||||||||||||||||||||||
| } { | ||||||||||||||||||||||||||||||||||||||
| const capacity = 1 << (32 - Math.clz32(Math.max(requestedCapacity, 2) - 1)); | ||||||||||||||||||||||||||||||||||||||
| const mask = capacity - 1; | ||||||||||||||||||||||||||||||||||||||
| return { capacity, mask }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export class SlotBitset128 { | ||||||||||||||||||||||||||||||||||||||
| private mask: bigint; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| constructor(initialMask: bigint = 0n) { | ||||||||||||||||||||||||||||||||||||||
| this.mask = initialMask & ((1n << 128n) - 1n); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| get rawMask(): bigint { | ||||||||||||||||||||||||||||||||||||||
| return this.mask; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| occupySlot(slotIndex: number): void { | ||||||||||||||||||||||||||||||||||||||
| if (slotIndex < 0 || slotIndex >= 128) return; | ||||||||||||||||||||||||||||||||||||||
| this.mask |= 1n << BigInt(slotIndex); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| freeSlot(slotIndex: number): void { | ||||||||||||||||||||||||||||||||||||||
| if (slotIndex < 0 || slotIndex >= 128) return; | ||||||||||||||||||||||||||||||||||||||
| this.mask &= ~(1n << BigInt(slotIndex)); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| isSlotOccupied(slotIndex: number): boolean { | ||||||||||||||||||||||||||||||||||||||
| if (slotIndex < 0 || slotIndex >= 128) return true; | ||||||||||||||||||||||||||||||||||||||
| return (this.mask & (1n << BigInt(slotIndex))) !== 0n; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| static createSpanMask(startSlot: number, slotCount: number): bigint { | ||||||||||||||||||||||||||||||||||||||
| if (slotCount <= 0 || startSlot < 0 || startSlot + slotCount > 128) { | ||||||||||||||||||||||||||||||||||||||
| return 0n; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const span = (1n << BigInt(slotCount)) - 1n; | ||||||||||||||||||||||||||||||||||||||
| return span << BigInt(startSlot); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| hasCollision(startSlot: number, slotCount: number, bufferPaddingSlots = 0): boolean { | ||||||||||||||||||||||||||||||||||||||
| const totalSlots = slotCount + bufferPaddingSlots; | ||||||||||||||||||||||||||||||||||||||
| const requested = SlotBitset128.createSpanMask(startSlot, totalSlots); | ||||||||||||||||||||||||||||||||||||||
| if (requested === 0n) return true; | ||||||||||||||||||||||||||||||||||||||
| return (this.mask & requested) !== 0n; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Document the padding semantics and the tail behavior of
The padding also produces false collisions at the tail. Add a doc comment that states the padding direction and the boundary rule. ♻️ Proposed change+ /**
+ * Reports whether the span collides with an occupied slot.
+ * `bufferPaddingSlots` extends the span forward only. The padded span is
+ * clamped to the 128-slot range, so trailing padding does not block placement.
+ */
hasCollision(startSlot: number, slotCount: number, bufferPaddingSlots = 0): boolean {
- const totalSlots = slotCount + bufferPaddingSlots;
+ if (slotCount <= 0 || startSlot < 0 || startSlot + slotCount > 128) return true;
+ const totalSlots = Math.min(slotCount + bufferPaddingSlots, 128 - startSlot);
const requested = SlotBitset128.createSpanMask(startSlot, totalSlots);
if (requested === 0n) return true;
return (this.mask & requested) !== 0n;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| merge(other: SlotBitset128): SlotBitset128 { | ||||||||||||||||||||||||||||||||||||||
| return new SlotBitset128(this.mask | other.rawMask); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,310 +1,2 @@ | ||
| import { type Accessor, createSignal, getListener, getOwner, onCleanup } from "solid-js"; | ||
| import { isServer } from "solid-js/web"; | ||
|
|
||
| export type ScheduleCallback = <Args extends unknown[]>( | ||
| callback: (...args: Args) => void, | ||
| wait?: number, | ||
| ) => Scheduled<Args>; | ||
|
|
||
| export interface Scheduled<Args extends unknown[]> { | ||
| (...args: Args): void; | ||
| clear: VoidFunction; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a callback that is debounced and cancellable. The debounced callback is called on **trailing** edge. | ||
| * | ||
| * The timeout will be automatically cleared on root dispose. | ||
| * | ||
| * @param callback The callback to debounce | ||
| * @param wait The duration to debounce in milliseconds | ||
| * @returns The debounced function | ||
| * | ||
| * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#debounce | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const fn = debounce((message: string) => console.log(message), 250); | ||
| * fn('Hello!'); | ||
| * fn.clear() // clears a timeout in progress | ||
| * ``` | ||
| */ | ||
| export const debounce: ScheduleCallback = (callback, wait) => { | ||
| if (isServer) { | ||
| return Object.assign(() => void 0, { clear: () => void 0 }); | ||
| } | ||
| let timeoutId: ReturnType<typeof setTimeout> | undefined; | ||
| const clear = () => clearTimeout(timeoutId); | ||
| if (getOwner()) onCleanup(clear); | ||
| const debounced: typeof callback = (...args) => { | ||
| if (timeoutId !== undefined) clear(); | ||
| timeoutId = setTimeout(() => callback(...args), wait); | ||
| }; | ||
| return Object.assign(debounced, { clear }); | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a callback that is throttled and cancellable. The throttled callback is called on **trailing** edge. | ||
| * | ||
| * The timeout will be automatically cleared on root dispose. | ||
| * | ||
| * @param callback The callback to throttle | ||
| * @param wait The duration to throttle | ||
| * @returns The throttled callback trigger | ||
| * | ||
| * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#throttle | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const trigger = throttle((val: string) => console.log(val), 250); | ||
| * trigger('my-new-value'); | ||
| * trigger.clear() // clears a timeout in progress | ||
| * ``` | ||
| */ | ||
| export const throttle: ScheduleCallback = (callback, wait) => { | ||
| if (isServer) { | ||
| return Object.assign(() => void 0, { clear: () => void 0 }); | ||
| } | ||
|
|
||
| let isThrottled = false, | ||
| timeoutId: ReturnType<typeof setTimeout>, | ||
| lastArgs: Parameters<typeof callback>; | ||
|
|
||
| const throttled: typeof callback = (...args) => { | ||
| lastArgs = args; | ||
| if (isThrottled) return; | ||
| isThrottled = true; | ||
| timeoutId = setTimeout(() => { | ||
| callback(...lastArgs); | ||
| isThrottled = false; | ||
| }, wait); | ||
| }; | ||
|
|
||
| const clear = () => { | ||
| clearTimeout(timeoutId); | ||
| isThrottled = false; | ||
| }; | ||
| if (getOwner()) onCleanup(clear); | ||
|
|
||
| return Object.assign(throttled, { clear }); | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a callback throttled using `window.requestIdleCallback()`. ([MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)) | ||
| * | ||
| * The throttled callback is called on **trailing** edge. | ||
| * | ||
| * The timeout will be automatically cleared on root dispose. | ||
| * | ||
| * @param callback The callback to throttle | ||
| * @param maxWait maximum wait time in milliseconds until the callback is called | ||
| * @returns The throttled callback trigger | ||
| * | ||
| * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#scheduleidle | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const trigger = scheduleIdle((val: string) => console.log(val), 250); | ||
| * trigger('my-new-value'); | ||
| * trigger.clear() // clears a timeout in progress | ||
| * ``` | ||
| */ | ||
| export const scheduleIdle: ScheduleCallback = isServer | ||
| ? () => Object.assign(() => void 0, { clear: () => void 0 }) | ||
| : // requestIdleCallback is not supported in Safari | ||
| typeof requestIdleCallback !== "undefined" | ||
| ? (callback, maxWait) => { | ||
| let isDeferred = false, | ||
| id: ReturnType<typeof requestIdleCallback>, | ||
| lastArgs: Parameters<typeof callback>; | ||
|
|
||
| const deferred: typeof callback = (...args) => { | ||
| lastArgs = args; | ||
| if (isDeferred) return; | ||
| isDeferred = true; | ||
| id = requestIdleCallback( | ||
| () => { | ||
| callback(...lastArgs); | ||
| isDeferred = false; | ||
| }, | ||
| { timeout: maxWait }, | ||
| ); | ||
| }; | ||
|
|
||
| const clear = () => { | ||
| cancelIdleCallback(id); | ||
| isDeferred = false; | ||
| }; | ||
| if (getOwner()) onCleanup(clear); | ||
|
|
||
| return Object.assign(deferred, { clear }); | ||
| } | ||
| : // fallback to setTimeout (throttle) | ||
| callback => throttle(callback); | ||
|
|
||
| /** | ||
| * Creates a scheduled and cancellable callback that will be called on **leading** edge. | ||
| * | ||
| * The timeout will be automatically cleared on root dispose. | ||
| * | ||
| * @param schedule {@link debounce} or {@link throttle} | ||
| * @param callback The callback to debounce/throttle | ||
| * @param wait timeout duration | ||
| * @returns The scheduled callback trigger | ||
| * | ||
| * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#leading | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const trigger = leading(throttle, (val: string) => console.log(val), 250); | ||
| * trigger('my-new-value'); | ||
| * trigger.clear() // clears a timeout in progress | ||
| * ``` | ||
| */ | ||
| export function leading<Args extends unknown[]>( | ||
| schedule: ScheduleCallback, | ||
| callback: (...args: Args) => void, | ||
| wait?: number, | ||
| ): Scheduled<Args> { | ||
| if (isServer) { | ||
| let called = false; | ||
| const scheduled = (...args: Args) => { | ||
| if (called) return; | ||
| called = true; | ||
| callback(...args); | ||
| }; | ||
| return Object.assign(scheduled, { clear: () => void 0 }); | ||
| } | ||
|
|
||
| let isScheduled = false; | ||
| const scheduled = schedule(() => (isScheduled = false), wait); | ||
|
|
||
| const func: typeof callback = (...args) => { | ||
| if (!isScheduled) callback(...args); | ||
| isScheduled = true; | ||
| scheduled(); | ||
| }; | ||
|
|
||
| const clear = () => { | ||
| isScheduled = false; | ||
| scheduled.clear(); | ||
| }; | ||
| if (getOwner()) onCleanup(clear); | ||
| return Object.assign(func, { clear }); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a scheduled and cancellable callback that will be called on the **leading** edge for the first call, and **trailing** edge for other calls. | ||
| * | ||
| * The timeout will be automatically cleared on root dispose. | ||
| * | ||
| * @param schedule {@link debounce} or {@link throttle} | ||
| * @param callback The callback to debounce/throttle | ||
| * @param wait timeout duration | ||
| * @returns The scheduled callback trigger | ||
| * | ||
| * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#leadingAndTrailing | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const trigger = leadingAndTrailing(throttle, (val: string) => console.log(val), 250); | ||
| * trigger('my-new-value'); | ||
| * trigger.clear() // clears a timeout in progress | ||
| * ``` | ||
| */ | ||
| export function leadingAndTrailing<Args extends unknown[]>( | ||
| schedule: ScheduleCallback, | ||
| callback: (...args: Args) => void, | ||
| wait?: number, | ||
| ): Scheduled<Args> { | ||
| if (isServer) { | ||
| let called = false; | ||
| const scheduled = (...args: Args) => { | ||
| if (called) return; | ||
| called = true; | ||
| callback(...args); | ||
| }; | ||
| return Object.assign(scheduled, { clear: () => void 0 }); | ||
| } | ||
|
|
||
| const enum State { | ||
| Ready, // 0 - default state, not scheduled | ||
| Leading, // 1 - scheduled, called leading edge, triggered only once | ||
| Trailing, // 2 - triggered more than once, will be called on trailing edge | ||
| } | ||
|
|
||
| let state = State.Ready; | ||
|
|
||
| const scheduled = schedule((args: Args) => { | ||
| state === State.Trailing && callback(...args); | ||
| state = State.Ready; | ||
| }, wait); | ||
|
|
||
| const fn: typeof callback = (...args) => { | ||
| if (state !== State.Trailing) { | ||
| if (state === State.Ready) callback(...args); | ||
| state += 1; | ||
| } | ||
| scheduled(args); | ||
| }; | ||
|
|
||
| const clear = () => { | ||
| state = State.Ready; | ||
| scheduled.clear(); | ||
| }; | ||
| if (getOwner()) onCleanup(clear); | ||
|
|
||
| return Object.assign(fn, { clear }); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a signal used for scheduling execution of solid computations by tracking. | ||
| * | ||
| * @param schedule Schedule the invalidate function (can be {@link debounce} or {@link throttle}) | ||
| * @returns A function used to track the signal. It returns `true` if the signal is dirty *(callback should be called)* and `false` otherwise. | ||
| * | ||
| * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#createScheduled | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const debounced = createScheduled(fn => debounce(fn, 250)); | ||
| * | ||
| * createEffect(() => { | ||
| * // track source signal | ||
| * const value = count(); | ||
| * // track the debounced signal and check if it's dirty | ||
| * if (debounced()) { | ||
| * console.log('count', value); | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
|
|
||
| // Thanks to Fabio Spampinato (https://github.com/fabiospampinato) for the idea for the primitive | ||
|
|
||
| export function createScheduled( | ||
| schedule: (callback: VoidFunction) => VoidFunction, | ||
| ): Accessor<boolean> { | ||
| let listeners = 0; | ||
| let isDirty = false; | ||
| const [track, dirty] = createSignal(void 0, { equals: false }); | ||
| const call = schedule(() => { | ||
| isDirty = true; | ||
| dirty(); | ||
| }); | ||
| return (): boolean => { | ||
| if (!isDirty) call(), track(); | ||
|
|
||
| if (isDirty) { | ||
| isDirty = !!listeners; | ||
| return true; | ||
| } | ||
|
|
||
| if (getListener()) { | ||
| listeners++; | ||
| onCleanup(() => listeners--); | ||
| } | ||
|
|
||
| return false; | ||
| }; | ||
| } | ||
| export * from "./index.js"; | ||
| export * from "./bitset.js"; | ||
|
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Locate the scheduled package sources and its declared entry points.
fd . packages/scheduled --type f --exclude node_modules
# Map the exported symbols of every source file in the package.
fd . packages/scheduled/src --type f --exec ast-grep outline {} \;
# Find the previous public exports of the package across the repo.
rg -n --type=ts -C2 '\bfrom\s+"`@solid-primitives/scheduled`"' -g '!**/node_modules/**'Repository: solidjs-community/solid-primitives Length of output: 1083 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- package metadata ---'
cat packages/scheduled/package.json
printf '%s\n' '--- current index and source files ---'
cat -n packages/scheduled/src/index.ts
cat -n packages/scheduled/src/bitset.ts
printf '%s\n' '--- package-local references to removed APIs ---'
rg -n -C2 '\b(debounce|throttle|scheduleIdle|leading|trailing|Scheduled)\b' packages/scheduled README.md packages 2>/dev/null | head -240
printf '%s\n' '--- change summary ---'
git diff --stat -- packages/scheduled
printf '%s\n' '--- changed paths ---'
git status --short -- packages/scheduledRepository: solidjs-community/solid-primitives Length of output: 24566 🏁 Script executed: #!/bin/bash
python3 - <<'PY'
import json
import re
from pathlib import Path
root = Path("packages/scheduled")
index = (root / "src/index.ts").read_text()
bitset = (root / "src/bitset.ts").read_text()
package = json.loads((root / "package.json").read_text())
index_exports = set()
for path, text in [(root / "src/index.ts", index), (root / "src/bitset.ts", bitset)]:
for match in re.finditer(r'export\s+(?:class|function|const|let|var|type|interface)\s+([A-Za-z_$][\w$]*)', text):
index_exports.add(match.group(1))
for match in re.finditer(r'export\s+\*\s+from\s+"([^"]+)"', text):
print(f"{path}: re-export {match.group(1)}")
expected = set(package["primitive"]["list"])
print("expected scheduling symbols:", sorted(expected))
print("symbols declared in current source:", sorted(index_exports))
print("expected symbols absent from current declarations:", sorted(expected - index_exports))
for test in sorted((root / "test").glob("*.ts")):
text = test.read_text()
for match in re.finditer(r'import\s*\{([^}]*)\}\s*from\s+"../src/index\.js"', text, re.S):
imported = {
item.replace("type ", "").strip().split(" as ")[-1]
for item in match.group(1).split(",")
if item.strip()
}
missing = imported - index_exports
if missing:
print(f"{test}: imports absent from current declarations: {sorted(missing)}")
PYRepository: solidjs-community/solid-primitives Length of output: 1177 Restore the scheduling exports from
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { | ||
| computePowerOfTwoMask, | ||
| SlotBitset128, | ||
| } from "../src/bitset"; | ||
|
|
||
| describe("Power-of-Two RingBuffer & 128-bit Slot Bitset", () => { | ||
| it("calculates exact power-of-two bitmasks", () => { | ||
| expect(computePowerOfTwoMask(100)).toEqual({ capacity: 128, mask: 127 }); | ||
| expect(computePowerOfTwoMask(1024)).toEqual({ capacity: 1024, mask: 1023 }); | ||
| }); | ||
|
|
||
| it("performs branchless 128-bit slot collision detection", () => { | ||
| const schedule = new SlotBitset128(); | ||
|
|
||
| schedule.occupySlot(10); | ||
| expect(schedule.isSlotOccupied(10)).toBe(true); | ||
| expect(schedule.isSlotOccupied(11)).toBe(false); | ||
|
|
||
| expect(schedule.hasCollision(8, 4)).toBe(true); | ||
| expect(schedule.hasCollision(11, 4)).toBe(false); | ||
| expect(schedule.hasCollision(8, 2, 1)).toBe(true); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Guard
computePowerOfTwoMaskagainst 32-bit shift overflow.<<uses 32-bit signed integers, and the shift count wraps at 32. ForrequestedCapacity = 2 ** 31, the shift is 31 andcapacitybecomes-2147483648withmask = -2147483649. ForrequestedCapacity = 2 ** 31 + 1, the shift is 32, socapacitybecomes1andmaskbecomes0. A non-finite input also returns{ capacity: 1, mask: 0 }becauseMath.clz32(NaN)is32. All three cases return a mask that no longer indexes a valid ringbuffer.Use exponentiation instead of the shift, and reject invalid input.
🐛 Proposed fix
export function computePowerOfTwoMask(requestedCapacity: number): { capacity: number; mask: number; } { - const capacity = 1 << (32 - Math.clz32(Math.max(requestedCapacity, 2) - 1)); + if (!Number.isFinite(requestedCapacity)) { + throw new RangeError("requestedCapacity must be a finite number"); + } + const requested = Math.max(Math.floor(requestedCapacity), 2); + if (requested > 2 ** 31) { + throw new RangeError("requestedCapacity exceeds the maximum supported capacity"); + } + const capacity = 2 ** (32 - Math.clz32(requested - 1)); const mask = capacity - 1; return { capacity, mask }; }📝 Committable suggestion
🤖 Prompt for AI Agents