Skip to content
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

Refs perf enhancement #238

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
21db161
Add new way of maintaining list of toPaths for greatly enhanced perfo…
cjblomqvist Dec 3, 2015
b7722ca
A lot of fixes to previous rework using smarter PathMap
cjblomqvist Dec 3, 2015
2e4c5e0
Add PathMap for from as well, in preparation for removing to and from…
cjblomqvist Dec 3, 2015
6a58da9
Additional fixes to rework where PathMap is used as FromMap for more …
cjblomqvist Dec 3, 2015
a07843b
Additional rework to support nested levels of items and paths in Path…
cjblomqvist Dec 3, 2015
1b2f09c
Use new fromPathMap instead of fromMap when dereferencing
cjblomqvist Dec 3, 2015
e0d02ca
Use new fromPathMap instead of fromMap for refs when converting to JS…
cjblomqvist Dec 3, 2015
59d801f
Remove fromMap completely from refs
cjblomqvist Dec 3, 2015
eac9fde
Fix syntax issue with new rework of Maps
cjblomqvist Dec 3, 2015
eb7b6d4
Use toPathMap instead of toMap with refs, part 1
cjblomqvist Dec 3, 2015
4b0b48d
Remove toMap from refs as it's no longer needed
cjblomqvist Dec 3, 2015
6e7dc66
Remove parentToMap and all other ToMap related stuff from refs, and u…
cjblomqvist Dec 3, 2015
3d92d52
Remove accidental added spacing
cjblomqvist Dec 3, 2015
5ec6707
Various minor fixes, including always returning arrays for PathListMaps
cjblomqvist Dec 4, 2015
5d8fee3
Merge commit 'f4ab92e1940a2769f8dbbdfb9f0f19a7283d5c6e' into refs-per…
cjblomqvist Jan 4, 2016
f9a8d96
Merge remote-tracking branch 'refs/remotes/derbyjs/master' into refs-…
cjblomqvist Jan 11, 2016
2719ad2
Merge remote-tracking branch 'refs/remotes/derbyjs/master' into refs-…
cjblomqvist Jan 11, 2016
8884498
Fix issues with cleanup and bundling of refs
cjblomqvist Feb 5, 2016
416ab16
Additional fix to refs cleanup/bundle issues
cjblomqvist Feb 5, 2016
bff89cf
Remove debugging param no longer applicable
cjblomqvist Feb 5, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 207 additions & 37 deletions lib/Model/ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ function addIndexListeners(model) {
onIndexChange(segments, patchMove);
});
function onIndexChange(segments, patch) {
var fromMap = model._refs.fromMap;
for (var from in fromMap) {
var ref = fromMap[from];
var toPathMap = model._refs.toPathMap;
var refs = toPathMap.get(segments) || [];

for(var i = 0, len = refs.length; i < len; i++) {
var ref = refs[i];
var from = ref.from;
if (!(ref.updateIndices &&
util.contains(segments, ref.toSegments) &&
ref.toSegments.length > segments.length)) continue;
var index = +ref.toSegments[segments.length];
var patched = patch(index);
Expand Down Expand Up @@ -111,15 +113,15 @@ function addListener(model, type, fn) {
// Find cases where an event is emitted on a path where a reference
// is pointing. All original mutations happen on the fully dereferenced
// location, so this detection only needs to happen in one direction
var toMap = model._refs.toMap;
var toPathMap = model._refs.toPathMap;
var subpath;
for (var i = 0, len = segments.length; i < len; i++) {
subpath = (subpath) ? subpath + '.' + segments[i] : segments[i];
// If a ref is found pointing to a matching subpath, re-emit on the
// place where the reference is coming from as if the mutation also
// occured at that path
var refs = toMap[subpath];
if (!refs) continue;
var refs = toPathMap.get(subpath.split('.'), true);
if (!refs.length) continue;
var remaining = segments.slice(i + 1);
for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) {
var ref = refs[refIndex];
Expand All @@ -138,9 +140,9 @@ function addListener(model, type, fn) {
}
// If a ref points to a child of a matching subpath, get the value in
// case it has changed and set if different
var parentToMap = model._refs.parentToMap;
var refs = parentToMap[subpath];
if (!refs) return;
var parentToPathMap = model._refs.parentToPathMap;
var refs = parentToPathMap.get(subpath.split('.'), true);
if (!refs.length) return;
for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) {
var ref = refs[refIndex];
var value = model._get(ref.toSegments);
Expand Down Expand Up @@ -208,9 +210,16 @@ Model.prototype.removeAllRefs = function(subpath) {
this._removeAllRefs(segments);
};
Model.prototype._removeAllRefs = function(segments) {
this._removeMapRefs(segments, this.root._refs.fromMap);
this._removePathMapRefs(segments, this.root._refs.fromPathMap);
this._removeMapRefs(segments, this.root._refLists.fromMap);
};
Model.prototype._removePathMapRefs = function(segments, map) {
var refs = map.getList(segments);
for(var i = 0, len = refs.length; i < len; i++) {
var ref = refs[i];
this._removeRef(ref.fromSegments, ref.from);
}
};
Model.prototype._removeMapRefs = function(segments, map) {
for (var from in map) {
var fromSegments = map[from].fromSegments;
Expand All @@ -227,7 +236,7 @@ Model.prototype.dereference = function(subpath) {

Model.prototype._dereference = function(segments, forArrayMutator, ignore) {
if (segments.length === 0) return segments;
var refs = this.root._refs.fromMap;
var refs = this.root._refs.fromPathMap;
var refLists = this.root._refLists.fromMap;
var doAgain;
do {
Expand All @@ -236,7 +245,7 @@ Model.prototype._dereference = function(segments, forArrayMutator, ignore) {
for (var i = 0, len = segments.length; i < len; i++) {
subpath = (subpath) ? subpath + '.' + segments[i] : segments[i];

var ref = refs[subpath];
var ref = refs.get(subpath.split('.'));
if (ref) {
var remaining = segments.slice(i + 1);
segments = ref.toSegments.concat(remaining);
Expand Down Expand Up @@ -278,53 +287,214 @@ function Ref(model, from, to, options) {
}
this.updateIndices = options && options.updateIndices;
}
function FromMap() {}
function ToMap() {}

function Refs() {
this.fromMap = new FromMap();
this.toMap = new ToMap();
this.parentToMap = new ToMap();
this.parentToPathMap = new PathListMap();
this.toPathMap = new PathListMap();
this.fromPathMap = new PathMap();
}

Refs.prototype.add = function(ref) {
this.fromMap[ref.from] = ref;
listMapAdd(this.toMap, ref.to, ref);
this.fromPathMap.add(ref.fromSegments, ref);
this.toPathMap.add(ref.toSegments, ref);
for (var i = 0, len = ref.parentTos.length; i < len; i++) {
listMapAdd(this.parentToMap, ref.parentTos[i], ref);
this.parentToPathMap.add(ref.parentTos[i].split('.'), ref);
}
};

Refs.prototype.remove = function(from) {
var ref = this.fromMap[from];
var ref = this.fromPathMap.get((from || '').split('.'));
if (!ref) return;
delete this.fromMap[from];
listMapRemove(this.toMap, ref.to, ref);
this.fromPathMap.delete(ref.fromSegments);
this.toPathMap.delete(ref.toSegments, ref);
for (var i = 0, len = ref.parentTos.length; i < len; i++) {
listMapRemove(this.parentToMap, ref.parentTos[i], ref);
this.parentToPathMap.delete(ref.parentTos[i].split('.'), ref);
}
return ref;
};

Refs.prototype.toJSON = function() {
var out = [];
for (var from in this.fromMap) {
var ref = this.fromMap[from];
var refs = this.fromPathMap.getList([]);

for(var i = 0, len = refs.length; i < len; i++) {
var ref = refs[i];
out.push([ref.from, ref.to]);
}
return out;
};

function listMapAdd(map, name, item) {
map[name] || (map[name] = []);
map[name].push(item);
function PathMap() {
this.map = {};
}

PathMap.prototype.add = function (segments, item) {
var map = this.map;

for(var i = 0, len = segments.length - 1; i < len; i++) {
map[segments[i]] = map[segments[i]] || {};
map = map[segments[i]];
}

map[segments[segments.length - 1]] = {"$item": item};
};

PathMap.prototype.get = function (segments) {
var val = this._get(segments);

return (val && val['$item']) ? val['$item'] : void 0;
};

PathMap.prototype._get = function (segments) {
var val = this.map;

for(var i = 0, len = segments.length; i < len; i++) {
val = val[segments[i]];
if(!val) return;
}

return val;
};

PathMap.prototype.getList = function (segments) {
var obj = this._get(segments);

return flattenObj(obj);
};

function flattenObj(obj) {
if(!obj) return [];

var arr = [];
var keys = Object.keys(obj);
if(obj['$item']) arr.push(obj['$item']);

for(var i = 0, len = keys.length; i < len; i++) {
if(keys[i] === '$item') continue;

arr = arr.concat(flattenObj(obj[keys[i]]));
}

return arr;
};

PathMap.prototype.delete = function (segments) {
del(this.map, segments.slice(0), true);
};

function del(map, segments, safe) {
var segment = segments.shift();

if(!segments.length) {
if(safe) {
delete map[segment];
return false;
} else {
return true;
}
}

var nextMap = map[segment];
if(!nextMap) return true;

var nextSafe = (Object.keys(nextMap).length > 1);
var remove = del(nextMap, segments, nextSafe);

if(remove) {
if(safe) {
delete map[segment];
return false;
} else {
return true;
}
}
}

function PathListMap() {
this.map = {};
}

PathListMap.prototype.add = function (segments, item) {
var map = this.map;

for(var i = 0, len = segments.length - 1; i < len; i++) {
map[segments[i]] = map[segments[i]] || {"$items": []};
map = map[segments[i]];
}

var segment = segments[segments.length - 1];

map[segment] = map[segment] || {"$items": []};
map[segment]['$items'].push(item);
};

PathListMap.prototype.get = function (segments, onlyAtLevel) {
var val = this.map;

for(var i = 0, len = segments.length; i < len; i++) {
val = val[segments[i]];
if(!val) return [];
}

if(onlyAtLevel) return (val['$items'] || []);

return flatten(val);
};

function flatten(obj) {
var arr = obj['$items'] || [];
var keys = Object.keys(obj);

for(var i = 0, len = keys.length; i < len; i++) {
if(keys[i] === '$items') continue;

arr.concat(flatten(obj[i]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little bug leading to crashes: the line should read

arr.concat(flatten(obj[keys[i]]));

}

return arr;
}

function listMapRemove(map, name, item) {
var items = map[name];
if (!items) return;
var index = items.indexOf(item);
if (index === -1) return;
items.splice(index, 1);
if (!items.length) delete map[name];
PathListMap.prototype.delete = function (segments, item) {
delList(this.map, segments.slice(0), item, true);
};

function delList(map, segments, item, safe) {
var segment = segments.shift();

if(!segments.length) {
if(!map[segment] || !map[segment]['$items']) return true;

var items = map[segment]['$items'];
var keys = Object.keys(map[segment]);

if(items.length < 2 && keys.length < 2) {
if(safe) {
delete map[segment];
return false;
} else {
return true;
}
} else {
var i = items.indexOf(item);

if(i > -1) items.splice(i, 1);

return false;
}
}

var nextMap = map[segment];
if(!nextMap) return true;

var nextSafe = (Object.keys(nextMap).length > 2 || nextMap['$items'].length);
var remove = delList(nextMap, segments, item, nextSafe);

if(remove) {
if(safe) {
delete map[segment];
return false;
} else {
return true;
}
}
}