deepFreeze (Object.freeze) (v2)

Revision 2 of this benchmark created on


Description

Setup

function deepFreeze(obj) {
  const stack = [obj];
  const visited = new WeakSet();
  
  while (stack.length > 0) {
    const current = stack.pop();
    
    if (visited.has(current)) continue;
    visited.add(current);
    
    Object.freeze(current);
    
    for (const key of Reflect.ownKeys(current)) {
      const value = current[key];
      if (
        value !== null &&
        (typeof value === "object" || typeof value === "function") &&
        !Object.isFrozen(value)
      ) {
        stack.push(value);
      }
    }
  }
  
  return obj;
}

function deepFreeze2(object) {
  // Retrieve the property names defined on object
  const propNames = Reflect.ownKeys(object);

  // Freeze properties before freezing self
  for (const name of propNames) {
    const value = object[name];

    if ((value && typeof value === "object") || typeof value === "function") {
      deepFreeze(value);
    }
  }

  return Object.freeze(object);
}

function cowFreeze(obj) {
  return new Proxy(obj, {
    set(target, prop, value) {
      throw new Error("Immutable: cannot modify frozen object");
    },
    get(target, prop) {
      const val = target[prop];
      if (val && typeof val === "object") {
        return cowFreeze(val); // lazily wrap nested objects
      }
      return val;
    }
  });
}

// Large fake object manufacturer
function createMockData({ depth = 4, breadth = 6, includeCycles = true, crossLinkRatio = 0.02 } = {}) {
  // nodes list to optionally create cross-links
  const nodes = [];
  
  function makeNode(level, path) {
    const node = { __id: path.join('.') };
    nodes.push(node);
    
    // add many primitive props to increase size
    for (let i = 0; i < Math.min(8, breadth); i++) {
      node['p' + i] = `val-${path.join('-')}-${i}`;
    }
    
    // add symbol keys occasionally
    if (level % 2 === 0) {
      const s = Symbol('sym' + path.join('-'));
      node[s] = `symval-${path.join('-')}`;
    }
    
    if (level < depth) {
      node.children = [];
      for (let b = 0; b < breadth; b++) {
        const child = makeNode(level + 1, path.concat(b));
        node.children.push(child);
      }
    } else {
      // leaf: add a large array to increase memory footprint
      node.leafArray = new Array(Math.min(200, breadth * 20)).fill(0).map((_, i) => `${path.join('-')}-item-${i}`);
    }
    
    return node;
  }
  
  const root = makeNode(0, ['root']);
  
  if (includeCycles) {
    // add some self-loops and cross-links
    for (let i = 0; i < Math.max(1, Math.floor(nodes.length * 0.005)); i++) {
      const idx = Math.floor(Math.random() * nodes.length);
      nodes[idx].self = nodes[idx]; // self-loop
    }
    
    // cross links based on ratio
    const crossCount = Math.floor(nodes.length * crossLinkRatio);
    for (let i = 0; i < crossCount; i++) {
      const a = nodes[Math.floor(Math.random() * nodes.length)];
      const b = nodes[Math.floor(Math.random() * nodes.length)];
      if (a && b && a !== b) {
        // attach a cross reference
        (a.crossRefs || (a.crossRefs = [])).push(b);
      }
    }
    
    // optional parent references using WeakMap style external map
    // (not attached to objects to avoid extra cycles unless desired)
  }
  
  return { root, _meta: { createdAt: Date.now(), nodeCount: nodes.length } };
}

const small = createMockData({ depth: 1, breadth: 2, includeCycles: false });
const big = createMockData(({ depth: 2, breadth: 3, includeCycles: true, crossLinkRatio: 0.005 }));

Test runner

Ready to run.

Testing in
TestOps/sec
(Small Object) Stack + WeakSet Cycle Detection
deepFreeze(structuredClone(small));
ready
(Small Object) Recursively Freeze
deepFreeze2(structuredClone(small));
ready
(Small Object) Copy-on-write
cowFreeze(structuredClone(small));
ready
(Big Object) Stack + WeakSet Cycle Detection
deepFreeze(structuredClone(big));
ready
(Big Object) Recursively Freeze
deepFreeze2(structuredClone(big));
ready
(Big Object) Proxy copy-on-write
cowFreeze(structuredClone(big));
ready

Revisions

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