signals icon indicating copy to clipboard operation
signals copied to clipboard

Allow a Signal to track multiple values using a bitmap (32bit integer)

Open eddyw opened this issue 2 years ago • 0 comments

Hey 👋

While my git command is unusable thanks to Xcode update, I thought to open a discussion about something I'm trying to hack together in my local signals clone repo.

From my understanding, a Node is sort of like a subscription, right? so it uses version instead of the actual value of the signal instance to know when a signal's value has changed.

Based on this idea, we could use a bitmap (a 32bit value) to track changes in 32 values. Each bitfield represents the position of the property, call it value0, value1, ..., value31 -> 0b1...111.

The idea is more or less this:

computed(() => {
  signal.value0; // Pos: 1 << 0
  signal.value1; // Pos: 1 << 1
  signal.value5; // Pos: 1 << 5
});

// (Pseudo) What roughly happens:

let fields = 0;  // Observed/watched fields
let version = 1; // Current version

fields |= 1 << 0; // <- get value0
fields |= 1 << 1; // <- get value1
fields |= 1 << 5; // <- get value5

computedNode.version = version;
computedNode.fields = fields; // 0b100011 Observed/watched fields by this `computed`
fields = 0;

This means, the current version is 1 and the signal values accessed are value0, value1, and value5 inside the computed. In other words, 1 single node is tracking changes in these 3 signal accessed fields using a single 32bit integer.

Now, when a value change occurs:

signal.value6 = "foo";

// (Pseudo) What roughly happens:

version += 1;
fields |= 1 << 6; // <- guess within batch could set multiple fields

if (
  computedNode.version !== version &&
  computedNode.fields & fields // it's '&' bitwise op, not '&&'
) computedNode.recompute()

In this particular case, value6 wouldn't cause the computed node to re-compute because - while the signal's version has changed - the observed values (0, 1, and 5) have not changed.

It's also fairly cheap to check which fields changed using a bitwise operator and v8 (at least) does a good job when you work only with 32bit integers (as long as the value is never set to float by accident).

For a bit of context, I have a factory that creates constructors, it adds getters/setters to the prototype which are wrapped in a signal. While signals have a relatively small memory footprint, I think this could be improved a lot. I use a similar approach to create 1 signal-like for every 32 properties. At best a single instance of a class has less than 32 properties so it can be used to observe all of them instead of creating multiple subscriptions for every property that's accessed in a computed for instance.

This makes sense only if all the properties are related because they're usually accessed together in a computed thing, for example (pseudo):

const internal = signalLike({ value0: "Bob", value1: "Alice" });
// .... just make some assumptions 😅
const user = new User(internal);

computed(() => {
  return user.firstname + " " + user.lastname;
}); // .value = "Bob Alice"

In this particular example, only 1 node would be created and bitfields set would be 1 << 0 and 1 << 1.

OK, so finally. I'm not really suggesting changing the API of Signal to have this awkward value{n} 😅. I'm maybe suggesting a lower primitive that can track up-to 32 (related/grouped) values. I don't know, I think this could allow building some really interesting things like user-land store-like objects which could track several (related) properties with a very low memory footprint. Also traversal of this Linked List would probably be faster for the best case where all observed properties belong to the same signal (or whatever this primitive could be called instead).

eddyw avatar Sep 25 '22 17:09 eddyw