working revision/changeset graph

findPath works. transition requests changesets from server. processing
of changesets works correctly.

Only need to manage the async-ifying of transition, which is currently
written as a while loop, (with a short-circuit break for debugging).
This commit is contained in:
s1341 2013-12-12 16:52:30 +02:00
parent 66c75e0fc9
commit 1eddedc434
2 changed files with 201 additions and 126 deletions

View file

@ -29,17 +29,31 @@ $.Class("Changeset",
{//statics
},
{//instance
init: function (deltarev, deltatime, value) {
this.deltarev = deltarev;
init: function (from_revision, to_revision, deltatime, value) {
this.from_revision = from_revision;
this.to_revision = to_revision;
this.deltatime = deltatime;
this.value = value;
},
getValue: function () {
return this.value;
},
/**
* 'Follow' the Changeset in a given direction, returning the revision at
* the specified end of the edge.
* @param {bool} direction - If true, go to the 'from' revision, otherwise
* go to the 'to' revision.
* @returns {Revision}
*/
follow: function (direction) {
if (direction)
return this.from_revision;
return this.to_revision;
}
}
);
//TODO: cruft! remove?
$.Class("DirectionalIterator",
{//statics
},
@ -77,7 +91,7 @@ $.Class("DirectionalIterator",
* making this significantly cheaper than applying all 250 changesets from r1
* to r251.
*/
$.Class("Revision2",
$.Class("Revision",
{//statics
// we rely on the fact that granularities are always traversed biggest to
// smallest. Changing this will break lots of stuff.
@ -99,70 +113,36 @@ $.Class("Revision2",
this.previous[granularity] = null;
}
},
addChangeset: function (target, changset, timedelta) {
/**
* Add a changeset from this revision to the target.
* @param {Revision} target - The target revision.
* @param {object} changeset - The raw changeset data.
* @param {time} timedelta - The difference in time between this revision
* and the target.
* @returns {Changeset} - The new changeset object.
*/
addChangeset: function (target, changeset, timedelta) {
console.log("[revision] addChangeset(%d -> %d)", this.revnum, target.revnum);
if (this.revnum == target.revnum)
// This should really never happen, but if it does, let's short-circuit.
return;
var delta_revnum = target.revnum - this.revnum;
// select the right edge set:
var edge = delta_revnum < 0 ? this.previous : this.next;
var direction_edges = delta_revnum < 0 ? this.previous : this.next;
// find the correct granularity and add an edge (changeset) for that granularity
for (var granularity in this.granularities) {
if (Math.abs(delta_revnum) == this.granularities[granularity]) {
for (var granularity in Revision.granularities) {
if (Math.abs(delta_revnum) == Revision.granularities[granularity]) {
//TODO: should we check whether the edge exists?
//TODO: modify changeset to store the REVISION, not the revnum.
edge[granularity] = new Changeset(target, timedelta, changeset);
return edge[granularity];
direction_edges[granularity] = new Changeset(this, target, timedelta, changeset);
return direction_edges[granularity];
}
}
// our delta_revnum isn't one of the granularities. Something is wrong
//TODO: handle this case?
return null;
},
findPath: function (target) {
//TODO: currently assuming only forward movements
var path = [];
var delta_revnum = target.revnum - this.revnum;
var edge = delta_revnum < 0 ? this.previous : this.next;
var current = this;
while (current.lt(target)) {
for (var granularity in this.granularities) {
if (Math.abs(current.revnum - target.revnum) >= this.granularities[granularity]) {
// the delta is larger than the granularity, let's use the granularity
//TODO: what happens if we DON'T have the edge?
// in theory we need to fetch it (and this is certainly the case for playback
// at granularity = 1). However, when skipping, we might try to find the NEXT
// Revision (which is not linked by the graph to current) and request revisions
// from current to that Revision (at the largest possible granularity)
var e = edge[granularity];
path.push(e);
current = e.target_revision;
break;
}
}
//TODO: none of the granularities matched. WTF? This probably means that there is NO useful
//edge in the direction of traversal. Is this a termination condition?
}
return path;
}
}
);
$.Class("Revision",
{//statics
},
{//instance
init: function (revnum) {
this.revnum = revnum;
this.changesets = [];
},
addChangeset: function (destindex, changeset, timedelta) {
this.changesets.push(new Changeset(destindex - this.revnum, timedelta, changeset));
this.changesets.sort(function (a, b) {
return (b.deltarev - a.deltarev);
});
},
lt: function (other, is_reverse) {
if (is_reverse)
return this.gt(other);
@ -190,77 +170,173 @@ $.Class("RevisionCache",
this.connection = connection;
this.loader = new ChangesetLoader(connection);
this.revisions = {};
this.head_revnum = head_revnum || 0;
this.head_revision = this.getRevision(head_revnum || 0);
this.loader.start();
},
/**
* Get a Revision instance for the specified revision number.
* If we don't yet have a Revision instance for this revision, create a
* new one. Also make sure that the head_revision attribute always refers
* to the instance for the pad's head revision.
* @param {number} revnum - The revision number for which we want a
* Revision object.
* @returns {Revision}
*/
getRevision: function (revnum) {
if (revnum in this.revisions)
return this.revisions[revnum];
this.revisions[revnum] = new Revision(revnum);
this.head_revnum = Math.max(this.head_revnum, revnum);
return this.revisions[revnum];
var revision = new Revision(revnum);
this.revisions[revnum] = revision;
if (this.head_revision && revnum > this.head_revision.revnum) {
this.head_revision = revision;
}
return revision;
},
findPath: function (from, to) {
var current_rev = this.getRevision(from);
var to_rev = this.getRevision(to);
var is_reverse = (from >= to);
var res = {
from: current_rev,
current: current_rev,
is_complete: false,
changesets: [],
};
if (from == to) {
//TODO: implement short-circuit
return res;
}
if (!res.current.changesets.length) {
// You cannot build a path if the starting revision hasn't
// got any changesets
//TODO: implement short-circuit
return res;
}
while (res.current.lt(to_rev, is_reverse)) {
var changeset_iterator = new DirectionalIterator(res.current.changesets, is_reverse);
while (changeset_iterator.haveNext()) {
var current_changeset = changeset_iterator.next();
// we might get stuck on a particular revision if only a
// partial path is available.
old_rev = res.current;
// the next (first) changeset in the current revision has a delta
// in the opposite direction to that in which we are trying to
// move, and so cannot help us. Because the changeset list is
// sorted, we can just stop here.
if (current_changeset.deltarev < 0) {
// When can this happen?
stop = true;
}
// the current revision has a changeset which helps us get to a revision
// which is closer to our target, so we should push that changeset to
// the list and move to that new revision to continue building a path
var delta_rev = this.getRevision(res.current.revnum + current_changeset.deltarev);
if (delta_rev.lt(to_rev, is_reverse)) {
res.changesets.push(current_changeset);
res.current = delta_rev;
break;
}
}
if (stop || res.current == old_rev)
break;
}
res.is_complete = res.current == to_rev;
return res;
},
addChangeset: function (from, to, value, reverseValue, timedelta) {
/**
* Links two revisions, specified by from and to with the changeset data
* in value and reverseValue respectively.
* @param {number} from - The revision number from which the forward
* changeset originates.
* @param {number} to - The revision number to which the forward changeset
* bring us.
* @param {changeset} forwardValue - The forward changeset data.
* @param {changeset} reverseValue - The reverse changeset data.
* @param {time} timedelta - The difference in time between the from and
* to revisions.
*/
addChangesetPair: function (from, to, value, reverseValue, timedelta) {
console.log("[revisioncache] addChangesetPair(%d, %d)", from, to);
var from_rev = this.getRevision(from);
var to_rev = this.getRevision(to);
from_rev.addChangeset(to, value, timedelta);
to_rev.addChangeset(from, reverseValue, -timedelta);
from_rev.addChangeset(to_rev, value, timedelta);
to_rev.addChangeset(from_rev, reverseValue, -timedelta);
},
/**
* Find a (minimal) path from a given revision to another revision. If a
* complete path cannot be found, return a path which comes as close as
* possible to the to revision.
* @param {Revision} from - The revision from which to start.
* @param {Revision} to - The revision which the path should try to reach.
* @returns {object} - A list of Changesets which describe a (partial) path
* from 'from' to 'to', and the last revision reached.
*/
findPath: function (from, to) {
/*
*TODO: currently we only ever move in the direction of sign(to-from).
*It might be worth implementing 'jitter' movements, so that if you,
*for example, you are trying to go from 0 to 99, and you have the
*following edges:
* 0 -> 100
* 100 -> 99
*The algorithm would be smart enough to provide you with that as a path
*/
var path = [];
var found_discontinuity = false;
var current = from;
while (current.lt(to, direction) && !found_discontinuity) {
var delta_revnum = to.revnum - current.revnum;
var direction_edges = direction ? current.previous : current.next;
var direction = delta_revnum < 0;
for (var granularity in Revision.granularities) {
if (Math.abs(delta_revnum) >= Revision.granularities[granularity]) {
/*
* the delta is larger than the granularity, let's use the granularity
*TODO: what happens if we DON'T have the edge?
* in theory we need to fetch it (and this is certainly the case for playback
* at granularity = 1). However, when skipping, we might try to find the NEXT
* Revision (which is not linked by the graph to current) and request revisions
* from current to that Revision (at the largest possible granularity)
*/
var edge = direction_edges[granularity];
if (edge) {
// add this edge to our path
path.push(edge);
// follow the edge to the next Revision node
current = edge.follow(direction);
// no need to look for smaller granularities
break;
} else {
// we don't have an edge. Normally we can just continue to the
// next granularity level. BUT, if we are at the lowest
// granularity and don't have an edge, we've reached a DISCONTINUITY
// and can no longer continue.
found_discontinuity = true;
}
}
}
}
// return either a full path, or a path ending as close as we can get to
// the target revision.
return {path: path, end_revision: current};
},
//TODO: horrible name!
transition: function (from_revnum, to_revnum, applyChangeset_callback) {
var path = [];
var current_revision = this.getRevision(from_revnum);
var target_revision = this.getRevision(to_revnum);
var count = 100;
//TODO: at the moment this is pretty much a busy loop. We should probably
//make the requestChangesets + applyChangeset_callback into some kind of
//chained (self-chaining?) async callback tail.
while (current_revision != target_revision && count--) {
var res = this.findPath(current_revision, target_revision);
current_revision = res.end_revision;
if (current_revision != target_revision) {
// we got a partial path. We need to request changesets from end_revision
this.requestChangesets(current_revision, target_revision);
}
//Apply the path we have, whether it is full or not:
if (applyChangeset_callback)
applyChangeset_callback(res.path);
console.log("[revisioncache] transition PATH:", res.path);
break;
}
},
/**
* Request changesets which will allow transitioning from 'from' to 'to'
* from the server.
* @param {Revision} from - The start revision.
* @param {Revision} to - The end revision.
*/
requestChangesets: function (from, to) {
console.log("[revisioncache] requestChangesets: %d -> %d", from.revnum, to.revnum);
var delta = to.revnum - from.revnum;
var sign = delta > 0 ? 1 : -1;
var start = delta > 0 ? from.revnum : to.revnum;
var end = delta > 0 ? to.revnum : from.revnum;
var adelta = Math.abs(delta);
var _this = this;
function process_received_changesets (data) {
console.log("[revisioncache] received changesets {from: %d, to: %d} @ granularity: %d", data.start, data.actualEndNum, data.granularity);
var start = data.start;
for (var i = 0; i < data.timeDeltas.length; i++, start += data.granularity) {
_this.addChangesetPair(start, start + data.granularity, data.forwardsChangesets[i], data.backwardsChangesets[i], data.timeDeltas[i]);
}
}
for (var g in Revision.granularities) {
var granularity = Revision.granularities[g];
var num = Math.floor(adelta / granularity);
console.log(start, granularity, num, adelta)
adelta = adelta % granularity;
if (num) {
//request at this granularity
// now we have to translate the request to a start + granularity. We know the granularity already
// so lets try to find an optimal start (which will give us what we want right now, and also provide
// a good 'spread' for later requests?
//start = start + (sign * (num * granularity));
this.loader.enqueue(start, granularity, process_received_changesets);
start = start + (sign * (num * granularity));
}
}
if (adelta) {
//Something went wrong!
}
},
/**
* Iterate over the list of changesets required to go from one revision to another.
@ -332,7 +408,7 @@ $.Class("ChangesetRequest",
init: function (start, granularity, callback) {
this.start = start;
this.granularity = granularity;
this.request_id = (this.start << 16) + granularity;
this.request_id = (this.granularity << 16) + this.start;
this.fulfill_callback = callback;
},
getRequestID: function () {
@ -358,7 +434,7 @@ Thread("ChangesetLoader",
* for communication with the server.
*/
init: function (connection) {
this._super(100);
this._super(2000);
this.connection = connection;
this.queues = {
small: [],
@ -397,6 +473,7 @@ Thread("ChangesetLoader",
* asked for.
*/
enqueue: function (start, granularity, callback) {
console.log("[changeset_loader] enqueue: %d, %d", start, granularity)
//TODO: check cache to see if we really need to fetch this
// maybe even to splices if we just need a smaller range
// in the middle
@ -439,10 +516,10 @@ Thread("ChangesetLoader",
}
//TODO: this stop is just for debugging!!!!
//FIXME: remove when done testing
this.stop();
//this.stop();
},
on_response: function (data) {
console.log("on_response: ", data);
console.log("[changesetloader] on_response: ", data);
if (!(data.requestID in this.pending)) {
console.log("[changesetloader] WTF? changeset not pending: ", data.requestID);
return;
@ -567,8 +644,6 @@ function loadBroadcastRevisionsJS(clientVars, connection)
var collabClientVars = clientVars.collab_client_vars;
p = new PadClient(collabClientVars.rev, collabClientVars.time, collabClientVars.initialAttributedText.text, collabClientVars.initialAttributedText.attribs, collabClientVars.apool);
cl = new ChangesetLoader(connection);
return cl;
}
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;

View file

@ -203,9 +203,9 @@ AuthenticatedSocketClient("TimesliderClient",
console.log("[timeslider_client] handle_COLLABROOM: ", data);
},
handle_CHANGESET_REQ: function(data) {
console.log("[timeslider_client] handle_CHANGESET_REQ: ", data);
},
//handle_CHANGESET_REQ: function(data) {
//console.log("[timeslider_client] handle_CHANGESET_REQ: ", data);
//},
}
);