Skip to content

Commit

Permalink
Optimize and benchmark circular references
Browse files Browse the repository at this point in the history
  • Loading branch information
trevorr committed Oct 8, 2024
1 parent a1ddca4 commit afa8b7b
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 44 deletions.
64 changes: 64 additions & 0 deletions benchmark/circles-fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict'

function buildPeopleGraph () {
const people = []

function createPerson (name, age) {
return {
name,
age,
friends: [],
parents: [],
children: []
}
}

// Populate the array with people
people.push(createPerson('John', 50))
people.push(createPerson('Jane', 48))
people.push(createPerson('Mike', 28))
people.push(createPerson('Sara', 26))
people.push(createPerson('Tom', 30))
people.push(createPerson('Lily', 27))
people.push(createPerson('Emily', 5))
people.push(createPerson('Jack', 3))
people.push(createPerson('Alice', 65))
people.push(createPerson('Bob', 66))

// Define relationships

// John and Jane are parents of Mike and Sara
people[0].children.push(people[2], people[3]) // John's children
people[1].children.push(people[2], people[3]) // Jane's children
people[2].parents.push(people[0], people[1]) // Mike's parents
people[3].parents.push(people[0], people[1]) // Sara's parents

// Tom and Lily are parents of Emily and Jack
people[4].children.push(people[6], people[7]) // Tom's children
people[5].children.push(people[6], people[7]) // Lily's children
people[6].parents.push(people[4], people[5]) // Emily's parents
people[7].parents.push(people[4], people[5]) // Jack's parents

// Alice and Bob are John's and Jane's parents (grandparents)
people[0].parents.push(people[8], people[9]) // John's parents
people[1].parents.push(people[8], people[9]) // Jane's parents
people[8].children.push(people[0], people[1]) // Alice's children
people[9].children.push(people[0], people[1]) // Bob's children

// Add friends relationships (mutual circular relationships)
people[2].friends.push(people[4]) // Mike and Tom are friends
people[4].friends.push(people[2])

people[3].friends.push(people[5]) // Sara and Lily are friends
people[5].friends.push(people[3])

people[0].friends.push(people[9]) // John and Bob (his father) are friends
people[9].friends.push(people[0])

people[1].friends.push(people[8]) // Jane and Alice (her mother) are friends
people[8].friends.push(people[1])

return people
}

module.exports = buildPeopleGraph()
61 changes: 61 additions & 0 deletions benchmark/circles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict'

/* global structuredClone */

const bench = require('fastbench')
const lodashCloneDeep = require('lodash.clonedeep')
const fastCopy = require('fast-copy').default
const obj = require('./circles-fixture')
const clone = require('..')
const nanoCopy = require('nano-copy')
const ramdaClone = require('ramda').clone
const cloneCircles = clone({ circles: true })
const cloneCirclesProto = clone({ circles: true, proto: true })
const max = 1000

const run = bench([
function benchLodashCloneDeep (cb) {
for (let i = 0; i < max; i++) {
lodashCloneDeep(obj)
}
setImmediate(cb)
},
function benchFastCopy (cb) {
for (let i = 0; i < max; i++) {
fastCopy(obj)
}
setImmediate(cb)
},
function benchNanoCopy (cb) {
for (let i = 0; i < max; i++) {
nanoCopy(obj)
}
setImmediate(cb)
},
function benchRamdaClone (cb) {
for (let i = 0; i < max; i++) {
ramdaClone(obj)
}
setImmediate(cb)
},
function benchRfdcCircles (cb) {
for (let i = 0; i < max; i++) {
cloneCircles(obj)
}
setImmediate(cb)
},
function benchRfdcCirclesProto (cb) {
for (let i = 0; i < max; i++) {
cloneCirclesProto(obj)
}
setImmediate(cb)
},
function benchStructuredClone (cb) {
for (let i = 0; i < max; i++) {
structuredClone(obj)
}
setImmediate(cb)
}
], 100)

run(run)
94 changes: 50 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,10 @@ function rfdc (opts) {
}

function rfdcCircles (opts) {
const refs = []
const refsNew = []

const constructorHandlers = new Map()
constructorHandlers.set(Date, (o) => new Date(o))
constructorHandlers.set(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn)))
constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn)))
constructorHandlers.set(Map, (o, refs, fn) => new Map(cloneArray(Array.from(o), refs, fn)))
constructorHandlers.set(Set, (o, refs, fn) => new Set(cloneArray(Array.from(o), refs, fn)))
if (opts.constructorHandlers) {
for (const handler of opts.constructorHandlers) {
constructorHandlers.set(handler[0], handler[1])
Expand All @@ -109,90 +106,99 @@ function rfdcCircles (opts) {
let handler = null
return opts.proto ? cloneProto : clone

function cloneArray (a, fn) {
function cloneArray (a, refs, fn) {
const keys = Object.keys(a)
const a2 = new Array(keys.length)
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
const cur = a[k]
if (typeof cur !== 'object' || cur === null) {
a2[k] = cur
} else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
a2[k] = handler(cur, fn)
} else if (ArrayBuffer.isView(cur)) {
a2[k] = copyBuffer(cur)
} else {
const index = refs.indexOf(cur)
if (index !== -1) {
a2[k] = refsNew[index]
const ref = refs.get(cur)
if (ref !== undefined) {
a2[k] = ref
} else {
a2[k] = fn(cur)
let cur2
if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
cur2 = handler(cur, refs, fn)
} else if (ArrayBuffer.isView(cur)) {
cur2 = copyBuffer(cur)
} else {
cur2 = fn(cur, refs)
}
refs.set(cur, cur2)
a2[k] = cur2
}
}
}
return a2
}

function clone (o) {
function clone (o, refs = new Map()) {
if (typeof o !== 'object' || o === null) return o
if (Array.isArray(o)) return cloneArray(o, clone)
if (Array.isArray(o)) return cloneArray(o, refs, clone)
if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) {
return handler(o, clone)
return handler(o, refs, clone)
}
const o2 = {}
refs.push(o)
refsNew.push(o2)
refs.set(o, o2)
for (const k in o) {
if (Object.hasOwnProperty.call(o, k) === false) continue
const cur = o[k]
if (typeof cur !== 'object' || cur === null) {
o2[k] = cur
} else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
o2[k] = handler(cur, clone)
} else if (ArrayBuffer.isView(cur)) {
o2[k] = copyBuffer(cur)
} else {
const i = refs.indexOf(cur)
if (i !== -1) {
o2[k] = refsNew[i]
const ref = refs.get(cur)
if (ref !== undefined) {
o2[k] = ref
} else {
o2[k] = clone(cur)
let cur2
if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
cur2 = handler(cur, refs, clone)
} else if (ArrayBuffer.isView(cur)) {
cur2 = copyBuffer(cur)
} else {
cur2 = clone(cur, refs)
}
refs.set(cur, cur2)
o2[k] = cur2
}
}
}
refs.pop()
refsNew.pop()
return o2
}

function cloneProto (o) {
function cloneProto (o, refs = new Map()) {
if (typeof o !== 'object' || o === null) return o
if (Array.isArray(o)) return cloneArray(o, cloneProto)
if (Array.isArray(o)) return cloneArray(o, refs, cloneProto)
if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) {
return handler(o, cloneProto)
return handler(o, refs, cloneProto)
}
const o2 = {}
refs.push(o)
refsNew.push(o2)
refs.set(o, o2)
for (const k in o) {
const cur = o[k]
if (typeof cur !== 'object' || cur === null) {
o2[k] = cur
} else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
o2[k] = handler(cur, cloneProto)
} else if (ArrayBuffer.isView(cur)) {
o2[k] = copyBuffer(cur)
} else {
const i = refs.indexOf(cur)
if (i !== -1) {
o2[k] = refsNew[i]
const ref = refs.get(cur)
if (ref !== undefined) {
o2[k] = ref
} else {
o2[k] = cloneProto(cur)
let cur2
if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
cur2 = handler(cur, refs, cloneProto)
} else if (ArrayBuffer.isView(cur)) {
cur2 = copyBuffer(cur)
} else {
cur2 = cloneProto(cur, refs)
}
refs.set(cur, cur2)
o2[k] = cur2
}
}
}
refs.pop()
refsNew.pop()
return o2
}
}
21 changes: 21 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ test('custom constructor handler', async ({ same, ok, isNot }) => {
same(cloned.foo.s, data.foo.s, 'same values')
isNot(cloned.foo, data.foo, 'different objects')
})
test('custom constructor handler - circular objects', async ({ same, ok, is, isNot }) => {
class Foo {
constructor (s) {
this.s = s
}
}
const a = { foo: new Foo('foo1') }
const b = { foo: new Foo('foo2') }
a.other = b
b.other = a
const data = { a, b }
const cloned = rfdc({ circles: true, constructorHandlers: [[Foo, (o) => new Foo(o.s)]] })(data)
ok(cloned.a.foo instanceof Foo)
ok(cloned.b.foo instanceof Foo)
same(cloned.a.foo.s, data.a.foo.s, 'same values')
same(cloned.b.foo.s, data.b.foo.s, 'same values')
isNot(cloned.a.foo, data.a.foo, 'different objects')
isNot(cloned.b.foo, data.b.foo, 'different objects')
is(cloned.a.other, cloned.b, 'same objects')
is(cloned.b.other, cloned.a, 'same objects')
})
test('custom RegExp handler', async ({ same, ok, isNot }) => {
const data = { regex: /foo/ }
const cloned = rfdc({ constructorHandlers: [[RegExp, (o) => new RegExp(o)]] })(data)
Expand Down

0 comments on commit afa8b7b

Please sign in to comment.