The server has some caching going on, but it targets control buses. The class has some caching (the “flat” representation, which is cheap) and manages targets at node tree information. Using Dictionary to test efficiency and memory with more frequent updates
(This is just a sketch for me (at least), it may be helpful to isolate it for testing ideas)
EDIT:
A version with a possible idea for optimization can be found below
NodeArrayCache {
var <rawCache, <lastUpdate, <updateInterval;
var server, updateFunc;
var <>onUpdate;
*new { |server|
^super.new.init(server);
}
init { |inServer|
server = inServer ? Server.default;
rawCache = nil;
lastUpdate = 0;
updateInterval = 1;
updateFunc = OSCFunc({ |msg|
rawCache = msg;
lastUpdate = Main.elapsedTime;
onUpdate.value(msg);
"Cache updated at %\n".postf(lastUpdate);
}, '/g_queryTree.reply', server.addr);
}
setUpdateInterval { |interval|
updateInterval = interval;
}
forceUpdate { |action|
lastUpdate = 0;
this.update(action);
}
updateOnNodeChange { |node|
if(node.isKindOf(Node)) {
this.forceUpdate({ |cache|
"Cache updated due to node % change\n".postf(node.nodeID);
});
};
}
watchNode { |node|
if(node.isKindOf(Node)) {
node.onFree({
this.updateOnNodeChange(node);
});
};
}
update { |action|
var now = Main.elapsedTime;
if((now - lastUpdate) >= updateInterval) {
server.queryAllNodes(true, { |msg|
rawCache = msg;
lastUpdate = now;
onUpdate.value(msg);
action.value(msg);
});
} {
action.value(rawCache);
};
}
startPeriodicUpdate { |interval|
var routine;
this.setUpdateInterval(interval);
routine = Routine({
loop {
this.forceUpdate;
interval.wait;
};
}).play;
^routine;
}
findNode { |nodeID, action|
this.update({ |cache|
var i = 2;
var result, searchFunc;
if(cache.isNil) {
"Cache is empty".warn;
^action.value(nil);
};
searchFunc = { |numChildren|
var found = nil;
numChildren.do {
var currentID = cache[i];
var isGroup = cache[i + 1] >= 0;
if(currentID == nodeID) {
found = this.extractNodeInfo(i);
^found;
};
if(isGroup) {
var children = cache[i + 1];
i = i + 2;
if(children > 0) {
found = searchFunc.value(children);
if(found.notNil) { ^found };
};
} {
var numControls = cache[i + 3];
i = i + 4 + (numControls * 2);
};
};
^found;
};
result = searchFunc.value(cache[3]);
action.value(result);
});
}
extractNodeInfo { |index|
var nodeID, typeFlag, isGroup;
if(rawCache.isNil) { ^nil };
nodeID = rawCache[index];
typeFlag = rawCache[index + 1];
isGroup = typeFlag >= 0;
^if(isGroup) {
(
\type: \group,
\id: nodeID,
\numChildren: typeFlag
)
} {
var defName = rawCache[index + 2];
var numControls = rawCache[index + 3];
var controls = Dictionary.new;
if(numControls > 0) {
numControls.do { |i|
var ctlIndex = index + 4 + (i * 2);
controls[rawCache[ctlIndex]] = rawCache[ctlIndex + 1];
};
};
(
\type: \synth,
\id: nodeID,
\defName: defName,
\controls: controls
)
};
}
getGroupChildren { |groupID, action|
this.update({ |cache|
var searchFunc;
var i = 2;
var children = List.new;
if(cache.isNil) {
"Cache is empty".warn;
^action.value([]);
};
searchFunc = { |numChildren|
var found = false;
numChildren.do {
var currentID = cache[i];
var isGroup = cache[i + 1] >= 0;
if(found) {
children.add(this.extractNodeInfo(i));
};
if(currentID == groupID) {
found = true;
};
if(isGroup) {
var numKids = cache[i + 1];
i = i + 2;
if(numKids > 0) { searchFunc.value(numKids) };
} {
var numControls = cache[i + 3];
i = i + 4 + (numControls * 2);
};
};
};
searchFunc.value(cache[3]);
action.value(children);
});
}
invalidate {
rawCache = nil;
lastUpdate = 0;
}
free {
updateFunc.free;
}
printCache {
if(rawCache.notNil) {
"Raw Cache Contents:".postln;
rawCache.postln;
} {
"Cache is empty".postln;
};
}
}
version with some attempts at optimization
NodeArrayCache {
var <rawCache, <lastUpdate, <updateInterval;
var server, updateFunc;
var <>onUpdate;
var nodeCache, groupCache, pathCache;
var nodeInfoPool;
*new { |server|
^super.new.init(server);
}
init { |inServer|
server = inServer ? Server.default;
nodeInfoPool = Array.fill(32, { Event.new });
this.clearCaches;
lastUpdate = 0;
updateInterval = 1;
updateFunc = OSCFunc({ |msg|
this.processNodeTree(msg);
onUpdate.value(msg);
"Cache updated at %\n".postf(lastUpdate);
}, '/g_queryTree.reply', server.addr);
}
getNodeInfo {
^(nodeInfoPool ?? { () })
}
recycleNodeInfo { |info|
if(nodeInfoPool.size < 64) {
nodeInfoPool = nodeInfoPool.add(info.clear);
}
}
clearCaches {
rawCache = nil;
nodeCache = IdentityDictionary.new;
groupCache = IdentityDictionary.new;
pathCache = IdentityDictionary.new;
}
processNodeTree { |msg|
rawCache = msg;
lastUpdate = Main.elapsedTime;
nodeCache.do { |info| this.recycleNodeInfo(info) };
nodeCache.clear;
groupCache.clear;
pathCache.clear;
this.prBuildCaches(msg[3], 2, [1]);
}
prBuildCaches { |numChildren, index, path|
var nextIndex = index;
numChildren.do {
var nodeID = rawCache[nextIndex];
var typeFlag = rawCache[nextIndex + 1];
var isGroup = typeFlag >= 0;
var nodeInfo;
nodeInfo = this.extractNodeInfo(nextIndex);
nodeCache[nodeID] = nodeInfo;
pathCache[nodeID] = path;
path.last.notNil.if {
groupCache[path.last] = groupCache[path.last] ?? { IdentitySet[] };
groupCache[path.last].add(nodeID);
};
if(isGroup) {
var numKids = typeFlag;
nextIndex = nextIndex + 2;
if(numKids > 0) {
nextIndex = this.prBuildCaches(
numKids,
nextIndex,
path ++ [nodeID]
);
};
} {
var numControls = rawCache[nextIndex + 3];
nextIndex = nextIndex + 4 + (numControls * 2);
};
};
^nextIndex;
}
extractNodeInfo { |index|
var nodeID, typeFlag, isGroup;
var info;
if(rawCache.isNil) { ^nil };
info = this.getNodeInfo;
nodeID = rawCache[index];
typeFlag = rawCache[index + 1];
isGroup = typeFlag >= 0;
if(isGroup) {
info.put(\type, \group)
.put(\id, nodeID)
.put(\numChildren, typeFlag);
} {
var defName = rawCache[index + 2];
var numControls = rawCache[index + 3];
var controls = Dictionary.new(numControls);
if(numControls > 0) {
numControls.do { |i|
var ctlIndex = index + 4 + (i * 2);
controls[rawCache[ctlIndex]] = rawCache[ctlIndex + 1];
};
};
info.put(\type, \synth)
.put(\id, nodeID)
.put(\defName, defName)
.put(\controls, controls);
};
^info;
}
findNode { |nodeID, action|
this.update({ |cache|
action.value(nodeCache[nodeID]);
});
}
getGroupChildren { |groupID, action|
this.update({ |cache|
var children = groupCache[groupID];
action.value(
if(children.notNil) {
children.asArray.collect { |id| nodeCache[id] }
} {
[]
}
);
});
}
setUpdateInterval { |interval|
updateInterval = interval;
}
forceUpdate { |action|
lastUpdate = 0;
this.update(action);
}
updateOnNodeChange { |node|
if(node.isKindOf(Node)) {
this.forceUpdate({ |cache|
"Cache updated due to node % change\n".postf(node.nodeID);
});
};
}
watchNode { |node|
if(node.isKindOf(Node)) {
node.onFree({
this.updateOnNodeChange(node);
});
};
}
update { |action|
var now = Main.elapsedTime;
if((now - lastUpdate) >= updateInterval) {
server.queryAllNodes(true, { |msg|
this.processNodeTree(msg);
action.value(rawCache);
});
} {
action.value(rawCache);
};
}
getNodePath { |nodeID, action|
this.update({ |cache|
action.value(pathCache[nodeID]);
});
}
startPeriodicUpdate { |interval|
var routine;
this.setUpdateInterval(interval);
routine = Routine({
loop {
this.forceUpdate;
interval.wait;
};
}).play;
^routine;
}
invalidate {
this.clearCaches;
lastUpdate = 0;
}
free {
updateFunc.free;
nodeInfoPool = nil;
}
printCache {
if(rawCache.notNil) {
"Raw Cache Contents:".postln;
rawCache.postln;
} {
"Cache is empty".postln;
};
}
}
quick test
~cache = NodeArrayCache(s);
~cache.onUpdate = { |msg|
"Cache updated with % nodes".postf(msg[3]);
};
g = Group.new;
x = Synth(\default, [\freq, 440]);
y = Synth(\default, [\freq, 880]);
~cache.findNode(x.nodeID, { |info|
"Synth info:".postln;
info.postln;
});
~cache.getGroupChildren(g.nodeID, { |children|
"Group children:".postln;
children.do(_.postln);
});
~updateRoutine = ~cache.startPeriodicUpdate(0.1); // Test with 0.1 refresh rate
~updateRoutine.stop;
~cache.free;