private void initState() { int numNodes = state.getNodeMap().size(); int numSlots = numNodes * slotsPerNode; this.legend = new Legend(null, null, 0); int latSides = (int) Math.ceil(Math.pow(numNodes, 1 / 3.0)); log.info("Lattice of " + numNodes + " requires " + latSides + " per side"); this.lattice = new Lattice<Lattice<Void>>(latSides); List<String> nodeNames = new ArrayList<String>(); List<Lattice<Void>> nodeLatti = new ArrayList<Lattice<Void>>(); for (GridNode node : state.getNodeMap().values()) { Lattice<Void> nodeLattice = new Lattice<Void>(nodeLatSides); List<String> slotNames = new ArrayList<String>(); List<Void> slots = new ArrayList<Void>(); for (int i = 0; i < slotsPerNode; i++) { slotNames.add("" + i); slots.add(null); } nodeLattice.addItems(slotNames, slots); nodeNames.add(node.getShortName()); nodeLatti.add(nodeLattice); // log.warn("Adding lattice for node "+node.getShortName()); } lattice.addItems(nodeNames, nodeLatti); // Initialize actors from the grid state for (GridNode node : state.getNodeMap().values()) { int s = 0; for (GridJob job : node.getSlots()) { if (job == null) continue; String slotName = s + ""; JobActor jobActor = createJobActor(job); jobActor.pos = getLatticePos(node.getShortName(), slotName); log.info( "Starting job {} on slot: {}", job.getFullJobId(), node.getShortName() + "#" + slotName); addJobActor(job.getFullJobId(), jobActor); s++; } } }
/** * The SketchState is a background thread responsible for drawing things which are relatively static * to a buffer. It has its own animation loop which tracks the timeline of the visualization at * whatever rate the user selects. Because it is decoupled from the main animation loop, it has no * specific timing it has to meet, so it can be relatively slow and precise. All of its drawing is * done with pixel precision, using Java 2D instead of OpenGL. * * <p>Anything which needs to be animated smoothly (e.g. job tweening) is left up to the enclosing * sketch, and its main animation loop. * * @author <a href="mailto:[email protected]">Konrad Rokicki</a> */ public class CubicSketchState implements Runnable { private static final Logger log = LoggerFactory.getLogger(CubicSketchState.class); // Timing private static final float DURATION_JOB_SUB = 40.0f; private static final float DURATION_JOB_START = 20.0f; private static final float DURATION_JOB_END = 70.0f; private static final float DISTANCE_JOB_START = 50.0f; // Invariants private final float width; private final float height; private final int slotsPerNode = 8; // Sizing variables private float nodeSize = 40; private float nodePadding = 5; private float jobPadding = 2; private int nodeLatSides = (int) Math.ceil(Math.pow(slotsPerNode, 1 / 3.0)); private float jobSize = (nodeSize - nodeLatSides * jobPadding) / nodeLatSides; private PApplet p; // Actors private Multimap<String, JobActor> jobActorMap = Multimaps.synchronizedMultimap(HashMultimap.<String, JobActor>create()); // Overall state private Legend legend; private Timeline timeline; // State for playing private PlayState playState = PlayState.PAUSED; private GridState state; private double playSpeed = 10.0f; private long prevElapsed; private long totalElapsed = 0; private Date lastSliceRequestDate; private long nextStartingPosition = 0; // Should changes to actors be tweened? This is usually disabled during buffering, for example. private boolean tweenChanges = true; public CubicSketchState(PApplet p, Timeline timeline, float width, float height) { this.p = p; this.width = width; this.height = height; this.timeline = timeline; } @Override public void run() { while (true) { switch (playState) { case BUFFERING: bufferToNextPosition(); break; case PLAYING: Date currDate = new Date(); long elapsed = (int) ((currDate.getTime() - lastSliceRequestDate.getTime()) * playSpeed); this.lastSliceRequestDate = currDate; // Check if we've been truncated, and move forward if necessary if (totalElapsed < timeline.getFirstOffset()) { log.info( "Elapsed time ({}) occurs before the current timeline ({})", totalElapsed, timeline.getFirstOffset()); long position = elapsed; boolean noMatch = true; while (noMatch) { if (position != timeline.getFirstOffset()) { position = timeline.getFirstOffset(); try { Thread.sleep(500); } catch (InterruptedException e) { // Ignore } } else { noMatch = false; } } log.info("Will buffer to new position: {}", position); bufferAtPosition(position); break; } // Update actors and build a usage map Map<String, Integer> slotsUsedByUser = new HashMap<String, Integer>(); Iterator<? extends Actor> i = getJobActors().values().iterator(); while (i.hasNext()) { Actor actor = i.next(); if (actor instanceof JobActor) { JobActor jobActor = (JobActor) actor; if (jobActor.defunct) { removeJobActor(jobActor.name, jobActor); } else { int slots = 1; if (jobActor.queued) { // If a job is queued then it is represented by a single sprite, so we need the // actual number of slots GridJob job = state.getJobByFullId(jobActor.name); if (job != null) { slots = job.getSlots(); } } if (jobActor.getName().contains(":")) { // Parse a jobId like this: 1275988.2828-4000:1 try { Pattern p = Pattern.compile("(\\d+)\\.(\\d+)-(\\d+):(\\d+)"); Matcher m = p.matcher(jobActor.getName()); if (m.matches()) { int start = Integer.parseInt(m.group(2)); int end = Integer.parseInt(m.group(3)); int interval = Integer.parseInt(m.group(4)); slots = (end - start) / interval; } } catch (Exception e) { log.error("Error parsing jobId: " + jobActor.getName(), e); } } else if (jobActor.getName().contains(",")) { // Parse a jobId like this: 2968157.205,211 try { Pattern p = Pattern.compile("(\\d+)\\.(\\d+),(\\d+)"); Matcher m = p.matcher(jobActor.getName()); if (m.matches()) { int first = Integer.parseInt(m.group(2)); int second = Integer.parseInt(m.group(3)); // There are two jobs listed here, so we require twice the number of slots slots *= 2; } } catch (Exception e) { log.error("Error parsing jobId: " + jobActor.getName(), e); } } String user = jobActor.getUsername(); if (!slotsUsedByUser.containsKey(user)) { slotsUsedByUser.put(user, 0); } if (!jobActor.queued) { slotsUsedByUser.put(user, slotsUsedByUser.get(user) + slots); } } } } legend.retain(slotsUsedByUser); updateState(elapsed); break; case READY: break; case PAUSED: break; case END: return; default: log.error("Invalid play state: " + playState); break; } try { Thread.sleep(50); } catch (InterruptedException e) { // Ignore } } } private void bufferToNextPosition() { log.info("Buffering to next position: {}", nextStartingPosition); int i = 0; Snapshot reqSnapshot = null; Snapshot prevSnapshot = null; for (Snapshot snapshot : timeline.getSnapshots()) { long offset = timeline.getOffset(snapshot.getSamplingTime()); log.debug("Snapshot {} has offset {}", i, offset); if (offset >= nextStartingPosition) { if (prevSnapshot == null) { reqSnapshot = snapshot; } else { reqSnapshot = prevSnapshot; } break; } prevSnapshot = snapshot; i++; } if (reqSnapshot == null) { reqSnapshot = prevSnapshot; if (reqSnapshot == null) { log.error("Could not find snapshot for offset {}", nextStartingPosition); setPlayState(PlayState.PAUSED); return; } } // Initialize the state at the closest possible snapshot log.info( "Init with snapshot with offset {}", timeline.getOffset(reqSnapshot.getSamplingTime())); this.state = new GridState(reqSnapshot, "runState"); initState(); // Apply all events between the closet snapshot and the desired starting position this.totalElapsed = timeline.getOffset((reqSnapshot.getSamplingTime())) + 1; // This must be set before calling updateState for the first time after changing the position // (i.e. totalElapsed) this.prevElapsed = totalElapsed; long elapsed = nextStartingPosition - totalElapsed; if (elapsed < 0) { log.warn( "Negative time elapsed. Normalizing to nextStartingPosition={}", nextStartingPosition); totalElapsed = nextStartingPosition; elapsed = nextStartingPosition; } log.info("Buffering elapsed: {}", elapsed); this.tweenChanges = false; updateState(elapsed); this.tweenChanges = true; // Get ready to start playing this.lastSliceRequestDate = new Date(); if (totalElapsed != nextStartingPosition) { totalElapsed = nextStartingPosition; } setPlayState(PlayState.READY); log.info("Buffered at totalElapsed={}", totalElapsed); } public synchronized void bufferAtPosition(long position) { if (playState == PlayState.PAUSED) { setPlayState(PlayState.BUFFERING); this.nextStartingPosition = position; } else { log.error("Cannot transition from " + playState + " to BUFFERING"); } } public synchronized void play() { if (playState == PlayState.PAUSED || playState == PlayState.READY) { log.info("Beginning playback at totalElapsed={}", totalElapsed); this.lastSliceRequestDate = new Date(); setPlayState(PlayState.PLAYING); } else { log.error("Cannot transition from " + playState + " to PLAYING"); } } public synchronized void pause() { if (playState == PlayState.PLAYING || playState == PlayState.PAUSED) { setPlayState(PlayState.PAUSED); } else { log.error("Cannot transition from " + playState + " to PAUSED"); } } public synchronized void end() { setPlayState(PlayState.END); } private void setPlayState(PlayState playState) { log.info("Entering state: {}", playState); this.playState = playState; } public boolean isPaused() { return playState == PlayState.PAUSED; } public boolean isReady() { return playState == PlayState.READY; } public boolean isPlaying() { return playState == PlayState.PLAYING; } public boolean isBuffering() { return playState == PlayState.BUFFERING; } public boolean isEnded() { return playState == PlayState.END; } public double getPlaySpeed() { return playSpeed; } public void setPlaySpeed(double playSpeed) { this.playSpeed = playSpeed; } public long getPosition() { return totalElapsed; } /** * Transliterated from Python solution available here: * http://stackoverflow.com/questions/6463297/algorithm-to-fill-rectangle-with-small-squares */ private int bestSquare(float w, float h, float n) { float hi = Math.max(w, h); float lo = 0; while (Math.abs(hi - lo) > 0.0001) { float mid = (lo + hi) / 2; float midval = (float) Math.floor(w / mid) * (float) Math.floor(h / mid); if (midval >= n) { lo = mid; } else if (midval < n) { hi = mid; } } return (int) Math.min(w / Math.floor(w / lo), h / Math.floor(h / lo)); } public class Lattice<T> { int size; Map<String, PVector> locationMap = new HashMap<String, PVector>(); Map<String, T> contentsMap = new HashMap<String, T>(); public Lattice(int size) { this.size = size; } public void addItems(List<String> names, List<T> items) { if (!locationMap.isEmpty()) { throw new IllegalStateException("addItems can only be called once for any given Lattice"); } Queue<String> nameQueue = new LinkedList<String>(names); Queue<T> itemQueue = new LinkedList<T>(items); for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { for (int k = 0; k < size; k++) { if (nameQueue.isEmpty() || itemQueue.isEmpty()) return; String name = nameQueue.remove(); T item = itemQueue.remove(); locationMap.put(name, new PVector(i, j, k)); contentsMap.put(name, item); } } } if (!nameQueue.isEmpty()) { log.warn( "Could not add all items to lattice of size " + size + ". Items remaining: " + nameQueue.size()); } } public PVector getLocation(String name) { return locationMap.get(name); } public T getContents(String name) { return contentsMap.get(name); } } private Lattice<Lattice<Void>> lattice; private void initState() { int numNodes = state.getNodeMap().size(); int numSlots = numNodes * slotsPerNode; this.legend = new Legend(null, null, 0); int latSides = (int) Math.ceil(Math.pow(numNodes, 1 / 3.0)); log.info("Lattice of " + numNodes + " requires " + latSides + " per side"); this.lattice = new Lattice<Lattice<Void>>(latSides); List<String> nodeNames = new ArrayList<String>(); List<Lattice<Void>> nodeLatti = new ArrayList<Lattice<Void>>(); for (GridNode node : state.getNodeMap().values()) { Lattice<Void> nodeLattice = new Lattice<Void>(nodeLatSides); List<String> slotNames = new ArrayList<String>(); List<Void> slots = new ArrayList<Void>(); for (int i = 0; i < slotsPerNode; i++) { slotNames.add("" + i); slots.add(null); } nodeLattice.addItems(slotNames, slots); nodeNames.add(node.getShortName()); nodeLatti.add(nodeLattice); // log.warn("Adding lattice for node "+node.getShortName()); } lattice.addItems(nodeNames, nodeLatti); // Initialize actors from the grid state for (GridNode node : state.getNodeMap().values()) { int s = 0; for (GridJob job : node.getSlots()) { if (job == null) continue; String slotName = s + ""; JobActor jobActor = createJobActor(job); jobActor.pos = getLatticePos(node.getShortName(), slotName); log.info( "Starting job {} on slot: {}", job.getFullJobId(), node.getShortName() + "#" + slotName); addJobActor(job.getFullJobId(), jobActor); s++; } } } private void updateState(long elapsed) { // Update the job actors List<GridEvent> events = getNextSlice(elapsed); if (!events.isEmpty()) { log.debug("Applying {} events", events.size()); for (GridEvent event : events) { applyEvent(event); } } } private List<GridEvent> getNextSlice(long elapsed) { List<GridEvent> slice = new ArrayList<GridEvent>(); if (elapsed <= 0) return slice; this.totalElapsed += elapsed; log.trace("getNextSlice, prevElapsed={}, totalElapsed={}", prevElapsed, totalElapsed); if (prevElapsed >= totalElapsed) { log.warn( "No slice possible with (prevElapsed={}, totalElapsed={})", prevElapsed, totalElapsed); return slice; } long start = prevElapsed; long end = totalElapsed; SortedMap<Long, List<Event>> eventSlice = timeline.getEvents(start, end); if (!eventSlice.isEmpty()) { // We only move the start of the window up when we find an event. This done because the // database might // have gaps if the incoming events cannot be processed in real-time. In that case, we don't // want to // miss any events if they come late. this.prevElapsed = totalElapsed; log.trace("Timeline has {} offset buckets", timeline.getNumOffsets()); log.info( "Requested slice where {}<=t<{} and got " + eventSlice.size() + " buckets", start, end); } if (!eventSlice.isEmpty()) { for (Long offset : eventSlice.keySet()) { log.trace("Got offset bucket {}", offset); if (offset >= totalElapsed) { log.warn( "Timeline returned grid events outside the requested frame: {}>{}", offset, totalElapsed); break; } List<Event> events = eventSlice.get(offset); synchronized (events) { if (events.isEmpty()) { log.trace("Got empty offset bucket for offset {}", offset); } for (Event event : events) { if (event instanceof GridEvent) { GridEvent gridEvent = (GridEvent) event; log.trace("Got grid event {}", gridEvent); slice.add(gridEvent); } else if (event instanceof SnapshotEvent) { SnapshotEvent gridEvent = (SnapshotEvent) event; log.trace("Got snapshot event {}", gridEvent); } else { log.trace("Got unrecognized event {}", event); } } } } } return slice; } private void applyEvent(GridEvent event) { String fullJobId = event.getJobId(); GridJob job = state.getJobByFullId(fullJobId); // Update the run state state.applyEvent(event); // Update the actor state switch (event.getType()) { case SUB: if (job == null) { job = new GridJob(event.getSnapshotJob()); } applySub(job); break; case START: if (job == null) { log.error("Cannot start null job"); } else { applyStart(job); } break; case END: if (job == null) { log.error("Cannot end null job"); } else { applyEnd(job, event.getOffset()); } break; default: log.warn("Unrecognized event type: {}", event.getType()); break; } } private void applySub(GridJob job) { // String fullJobId = job.getFullJobId(); // Collection<JobActor> actors = jobActorMap.get(fullJobId); // if (actors!=null && !actors.isEmpty()) { // log.warn("Ignoring sub event for known job {}",fullJobId); // return; // } // // log.debug("Adding queued job {}",fullJobId); } private void applyStart(GridJob job) { String fullJobId = job.getFullJobId(); Collection<JobActor> actors = jobActorMap.get(fullJobId); if (actors.isEmpty()) { JobActor jobActor = createJobActor(job); addJobActor(fullJobId, jobActor); actors = jobActorMap.get(fullJobId); } if (job.getNode() == null) { log.warn("No node for job being started: {}", fullJobId); return; } log.debug("Starting job {} on {}", fullJobId, job.getNode().getShortName()); String nodeName = job.getNode().getShortName(); if (actors.size() > 1) { log.warn("More than one actor for job being started: " + fullJobId); } JobActor jobActor = actors.iterator().next(); int i = 0; GridJob[] nodeJobs = job.getNode().getSlots(); for (int s = 0; s < nodeJobs.length; s++) { GridJob nodeJob = nodeJobs[s]; if (nodeJob == null) continue; if (!nodeJob.getFullJobId().equals(fullJobId)) continue; if (i > 0) { jobActor = cloneJobActor(fullJobId); } PVector endPos = getLatticePos(nodeName, s + ""); // TODO: random outside location in direction of vector from 0,0,0 PVector startPos = new PVector(1000, 1000, 1000); jobActor.pos = startPos; if (tweenChanges) { // scale duration to the distance that needs to be traveled float distance = jobActor.pos.dist(endPos); float duration = (DURATION_JOB_START * distance / DISTANCE_JOB_START) * 0.6f; Tween tween = new Tween("start_job_" + fullJobId + "#" + i, getTweenDuration(duration)) .addPVector(jobActor.pos, endPos) .call(jobActor, "jobStarted") .setEasing(Tween.SINE_OUT) .noAutoUpdate(); jobActor.tweens.add(tween); } else { jobActor.pos.set(endPos); } i++; } } private void applyEnd(GridJob job, long endOffset) { String fullJobId = job.getFullJobId(); Collection<JobActor> actors = jobActorMap.get(fullJobId); if (actors == null || actors.isEmpty()) { log.warn("Cannot end job that does not exist: {}", fullJobId); return; } log.debug("Finishing job {} on node {}", fullJobId, job.getNode().getShortName()); int i = 0; for (JobActor jobActor : actors) { PVector endPos = jobActor.getPos().get(); endPos.mult(10.0f); if (tweenChanges) { // scale duration to the distance that needs to be traveled float distance = jobActor.pos.dist(endPos); float duration = (DURATION_JOB_END * distance / DURATION_JOB_END) * 0.6f; Tween tween = new Tween("end_job_" + fullJobId + "#" + i, getTweenDuration(duration)) .addPVector(jobActor.pos, endPos) .call(jobActor, "jobEnded") .setEasing(Tween.SINE_IN) .noAutoUpdate(); jobActor.tweens.add(tween); } else { jobActor.jobEnded(); } i++; } } private JobActor createJobActor(GridJob job) { JobActor jobActor = new JobActor(null, job.getOwner()); jobActor.color = legend.getItemColor(job.getOwner()); jobActor.name = job.getFullJobId(); return jobActor; } private void addJobActor(String fullJobId, JobActor jobActor) { jobActorMap.put(fullJobId, jobActor); } private void removeJobActor(String fullJobId, JobActor jobActor) { jobActorMap.remove(fullJobId, jobActor); } private JobActor cloneJobActor(String fullJobId) { Collection<JobActor> actors = jobActorMap.get(fullJobId); if (actors == null || actors.isEmpty()) { log.error("Cannot expand actors for non-existing job: " + fullJobId); return null; } JobActor jobActor = actors.iterator().next(); // first actor GridJob job = state.getJobByFullId(fullJobId); if (job == null) { log.error("Cannot expand actors for unknown job: " + fullJobId); return null; } JobActor copy = jobActor.copy(); copy.name += "#" + actors.size(); jobActorMap.put(fullJobId, copy); return copy; } private PVector getLatticePos(String nodeName, String slotName) { PVector nodeLoc = lattice.getLocation(nodeName); Lattice<Void> slotLat = lattice.getContents(nodeName); if (slotLat == null) { log.warn("No slot lattice for node " + nodeName); return new PVector(0, 0, 0); } // Node position within the lattice, natural coordinates float nodeOffset = nodeSize + 2 * nodePadding; PVector nodePos = nodeLoc.get(); nodePos.mult(nodeOffset); nodePos.add(nodePadding, nodePadding, nodePadding); PVector slotLoc = slotLat.getLocation(slotName); if (slotLoc == null) { log.warn("No slot location for slot " + slotName + " in node " + nodeName); return nodePos.get(); } // TODO: cache these calculations // Slot position within the node, natural coordinates float slotOffset = jobSize + 2 * jobPadding; PVector slotPos = slotLoc.get(); slotPos.mult(slotOffset); slotPos.add(jobPadding, jobPadding, jobPadding); // Combine node and slot position, natural coordinates PVector pos = slotPos.get(); pos.add(nodePos); // Convert into central coordinate system float latticeWidth = lattice.size * nodeOffset; float t = latticeWidth / 2; pos.sub(t, t, t); return pos; } private float getTweenDuration(float durationJobSub) { if (playSpeed <= 1) return durationJobSub; return durationJobSub / (float) Math.log(playSpeed); } public class Actor { protected PVector pos; public Actor(PVector pos) { this.pos = pos; } public PVector getPos() { return pos; } public void setPos(PVector pos) { this.pos = pos; } } public class JobActor extends Actor { protected String name; protected String username; protected int color = Utils.color("ff0000"); protected float opacity = 150; protected Set<Motion> tweens = new HashSet<Motion>(); protected boolean queued = false; protected boolean defunct = false; JobActor(PVector pos, String username) { super(pos); this.username = username; } public void update() { if (isInMotion()) { Set<Motion> toRemove = new HashSet<Motion>(); for (Motion m : tweens) { if (!m.isPlaying()) { if (m.getPosition() == 0) { m.play(); } else { toRemove.add(m); } } else { m.update(); } } for (Motion m : toRemove) { tweens.remove(m); } } } public boolean isInMotion() { return !tweens.isEmpty(); } public void draw(PGraphics buf) { if (pos == null) { log.warn("Actor {} has no position", name); return; } buf.pushMatrix(); buf.stroke(Utils.color("ffffff"), opacity); buf.strokeWeight(1); buf.fill(color, opacity); buf.translate(pos.x, pos.y, pos.z); buf.box(jobSize); buf.popMatrix(); } public JobActor copy() { JobActor copy = new JobActor(pos, username); copy.name = name; copy.color = this.color; return copy; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUsername() { return username; } public float getOpacity() { return opacity; } public void setOpacity(float opacity) { this.opacity = opacity; } public Set<Motion> getTweens() { return tweens; } public boolean isDefunct() { return defunct; } public void jobStarted() {} public void jobEnded() { defunct = true; color = Utils.color("000000"); // opacity = 0; // just in case } } public Multimap<String, JobActor> getJobActors() { synchronized (jobActorMap) { return ImmutableMultimap.copyOf(jobActorMap); } } public void setTimeline(Timeline timeline) { this.timeline = timeline; } }