From fa059c9d1e80460990ed94874db2eeb1a5deb8d6 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <cwillisf@media.mit.edu>
Date: Fri, 8 Jun 2018 12:38:10 -0700
Subject: [PATCH 1/4] Add CachedTimer, replace getTimer() calls

---
 src/Scratch.as                             | 75 +---------------------
 src/extensions/ExtensionManager.as         |  4 +-
 src/interpreter/Interpreter.as             | 13 ++--
 src/logging/LogEntry.as                    |  6 +-
 src/render3d/DisplayObjectContainerIn3D.as |  5 --
 src/scratch/ScratchObj.as                  |  2 +-
 src/scratch/ScratchRuntime.as              | 10 +--
 src/sound/ScratchSoundPlayer.as            |  6 +-
 src/ui/parts/LibraryPart.as                |  6 +-
 src/ui/parts/ScriptsPart.as                |  6 +-
 src/uiwidgets/Menu.as                      |  6 +-
 src/util/CachedTimer.as                    | 55 ++++++++++++++++
 src/util/GestureHandler.as                 | 12 ++--
 src/util/Perf.as                           | 55 ----------------
 src/util/Transition.as                     |  4 +-
 src/watchers/ListWatcher.as                |  8 ++-
 16 files changed, 104 insertions(+), 169 deletions(-)
 create mode 100644 src/util/CachedTimer.as
 delete mode 100644 src/util/Perf.as

diff --git a/src/Scratch.as b/src/Scratch.as
index 3b03154..b8bd666 100644
--- a/src/Scratch.as
+++ b/src/Scratch.as
@@ -734,6 +734,7 @@ public class Scratch extends Sprite {
 
 	protected function step(e:Event):void {
 		// Step the runtime system and all UI components.
+		CachedTimer.clearCachedTimer();
 		gh.step();
 		runtime.stepRuntime();
 		Transition.step(null);
@@ -971,10 +972,6 @@ public class Scratch extends Sprite {
 		scriptsPart.setWidthHeight(contentW, contentH);
 
 		if (mediaLibrary) mediaLibrary.setWidthHeight(topBarPart.w, fullH);
-		if (frameRateGraph) {
-			frameRateGraph.y = stage.stageHeight - frameRateGraphH;
-			addChild(frameRateGraph); // put in front
-		}
 
 		SCRATCH::allow3d {
 			if (isIn3D) render3D.onStageResize();
@@ -1497,76 +1494,6 @@ public class Scratch extends Sprite {
 		lp.y = int(p.y + ((stagePane.height - lp.height) / 2));
 	}
 
-	// -----------------------------
-	// Frame rate readout (for use during development)
-	//------------------------------
-
-	private var frameRateReadout:TextField;
-	private var firstFrameTime:int;
-	private var frameCount:int;
-
-	protected function addFrameRateReadout(x:int, y:int, color:uint = 0):void {
-		frameRateReadout = new TextField();
-		frameRateReadout.autoSize = TextFieldAutoSize.LEFT;
-		frameRateReadout.selectable = false;
-		frameRateReadout.background = false;
-		frameRateReadout.defaultTextFormat = new TextFormat(CSS.font, 12, color);
-		frameRateReadout.x = x;
-		frameRateReadout.y = y;
-		addChild(frameRateReadout);
-		frameRateReadout.addEventListener(Event.ENTER_FRAME, updateFrameRate);
-	}
-
-	private function updateFrameRate(e:Event):void {
-		frameCount++;
-		if (!frameRateReadout) return;
-		var now:int = getTimer();
-		var msecs:int = now - firstFrameTime;
-		if (msecs > 500) {
-			var fps:Number = Math.round((1000 * frameCount) / msecs);
-			frameRateReadout.text = fps + ' fps (' + Math.round(msecs / frameCount) + ' msecs)';
-			firstFrameTime = now;
-			frameCount = 0;
-		}
-	}
-
-	// TODO: Remove / no longer used
-	private const frameRateGraphH:int = 150;
-	private var frameRateGraph:Shape;
-	private var nextFrameRateX:int;
-	private var lastFrameTime:int;
-
-	private function addFrameRateGraph():void {
-		addChild(frameRateGraph = new Shape());
-		frameRateGraph.y = stage.stageHeight - frameRateGraphH;
-		clearFrameRateGraph();
-		stage.addEventListener(Event.ENTER_FRAME, updateFrameRateGraph);
-	}
-
-	public function clearFrameRateGraph():void {
-		var g:Graphics = frameRateGraph.graphics;
-		g.clear();
-		g.beginFill(0xFFFFFF);
-		g.drawRect(0, 0, stage.stageWidth, frameRateGraphH);
-		nextFrameRateX = 0;
-	}
-
-	private function updateFrameRateGraph(evt:*):void {
-		var now:int = getTimer();
-		var msecs:int = now - lastFrameTime;
-		lastFrameTime = now;
-		var c:int = 0x505050;
-		if (msecs > 40) c = 0xE0E020;
-		if (msecs > 50) c = 0xA02020;
-
-		if (nextFrameRateX > stage.stageWidth) clearFrameRateGraph();
-		var g:Graphics = frameRateGraph.graphics;
-		g.beginFill(c);
-		var barH:int = Math.min(frameRateGraphH, msecs / 2);
-		g.drawRect(nextFrameRateX, frameRateGraphH - barH, 1, barH);
-		nextFrameRateX++;
-	}
-
 	// -----------------------------
 	// Camera Dialog
 	//------------------------------
diff --git a/src/extensions/ExtensionManager.as b/src/extensions/ExtensionManager.as
index 488ea58..5a38fa5 100644
--- a/src/extensions/ExtensionManager.as
+++ b/src/extensions/ExtensionManager.as
@@ -403,7 +403,7 @@ public class ExtensionManager {
 
 	public function updateIndicator(indicator:IndicatorLight, ext:ScratchExtension, firstTime:Boolean = false):void {
 		if(ext.port > 0) {
-			var msecsSinceLastResponse:uint = getTimer() - ext.lastPollResponseTime;
+			var msecsSinceLastResponse:uint = CachedTimer.getCachedTimer() - ext.lastPollResponseTime;
 			if (msecsSinceLastResponse > 500) indicator.setColorAndMsg(0xE00000, 'Cannot find helper app');
 			else if (ext.problem != '') indicator.setColorAndMsg(0xE0E000, ext.problem);
 			else indicator.setColorAndMsg(0x00C000, ext.success);
@@ -724,7 +724,7 @@ public class ExtensionManager {
 
 	private function processPollResponse(ext:ScratchExtension, response:String):void {
 		if (response == null) return;
-		ext.lastPollResponseTime = getTimer();
+		ext.lastPollResponseTime = CachedTimer.getCachedTimer();
 		ext.problem = '';
 
 		// clear the busy list unless we just started a command that waits
diff --git a/src/interpreter/Interpreter.as b/src/interpreter/Interpreter.as
index 480f359..e27435c 100644
--- a/src/interpreter/Interpreter.as
+++ b/src/interpreter/Interpreter.as
@@ -70,10 +70,12 @@ import scratch.*;
 
 import sound.*;
 
+import util.CachedTimer;
+
 public class Interpreter {
 
-	public var activeThread:Thread;				// current thread
-	public var currentMSecs:int = getTimer();	// millisecond clock for the current step
+	public var activeThread:Thread;         // current thread
+	public var currentMSecs:int;            // millisecond clock for the current step
 	public var turboMode:Boolean = false;
 
 	private var app:Scratch;
@@ -220,10 +222,9 @@ public class Interpreter {
 	}
 
 	public function stepThreads():void {
-		startTime = getTimer();
 		var workTime:int = (0.75 * 1000) / app.stage.frameRate; // work for up to 75% of one frame time
 		doRedraw = false;
-		currentMSecs = getTimer();
+		startTime = currentMSecs = CachedTimer.getCachedTimer();
 		if (threads.length == 0) return;
 		while ((currentMSecs - startTime) < workTime) {
 			if (warpThread && (warpThread.block == null)) clearWarpBlock();
@@ -247,7 +248,7 @@ public class Interpreter {
 				threads = newThreads;
 				if (threads.length == 0) return;
 			}
-			currentMSecs = getTimer();
+			currentMSecs = CachedTimer.getFreshTimer();
 			if (doRedraw || (runnableCount == 0)) return;
 		}
 	}
@@ -265,7 +266,7 @@ public class Interpreter {
 		}
 		yield = false;
 		while (true) {
-			if (activeThread == warpThread) currentMSecs = getTimer();
+			if (activeThread == warpThread) currentMSecs = CachedTimer.getFreshTimer();
 			evalCmd(activeThread.block);
 			if (yield) {
 				if (activeThread == warpThread) {
diff --git a/src/logging/LogEntry.as b/src/logging/LogEntry.as
index 22940b4..0d9e173 100644
--- a/src/logging/LogEntry.as
+++ b/src/logging/LogEntry.as
@@ -20,6 +20,8 @@
 package logging {
 import flash.utils.getTimer;
 
+import util.CachedTimer;
+
 public class LogEntry {
 	public var timeStamp:Number;
 	public var severity:int;
@@ -49,11 +51,11 @@ public class LogEntry {
 		return [makeTimeStampString(), LogLevel.LEVEL[severity], messageKey].join(' | ');
 	}
 
-	private static const timerOffset:Number = new Date().time - getTimer();
+	private static const timerOffset:Number = new Date().time - CachedTimer.getFreshTimer();
 
 	// Returns approximately the same value as "new Date().time" without GC impact
 	public static function getCurrentTime():Number {
-		return getTimer() + timerOffset;
+		return CachedTimer.getCachedTimer() + timerOffset;
 	}
 }
 }
diff --git a/src/render3d/DisplayObjectContainerIn3D.as b/src/render3d/DisplayObjectContainerIn3D.as
index 3571583..53302a2 100644
--- a/src/render3d/DisplayObjectContainerIn3D.as
+++ b/src/render3d/DisplayObjectContainerIn3D.as
@@ -1052,12 +1052,8 @@ SCRATCH::allow3d{
 		currentTexture = null;
 	}
 
-	private var drawCount:uint = 0;
-	//private var lastTime:int = 0;
 	public function onRender(e:Event):void {
 		if (!scratchStage) return;
-		//trace('frame was '+(getTimer() - lastTime)+'ms.');
-		//lastTime = getTimer();
 
 		if (scratchStage.stage.stage3Ds[0] == null || __context == null || __context.driverInfo == "Disposed") {
 			if (__context) __context.dispose();
@@ -1068,7 +1064,6 @@ SCRATCH::allow3d{
 
 		draw();
 		__context.present();
-		++drawCount;
 
 		// Invalidate cached renders
 		for (var o:Object in cachedOtherRenderBitmaps)
diff --git a/src/scratch/ScratchObj.as b/src/scratch/ScratchObj.as
index a6fe8ac..419b74d 100644
--- a/src/scratch/ScratchObj.as
+++ b/src/scratch/ScratchObj.as
@@ -544,7 +544,7 @@ public class ScratchObj extends Sprite {
 	public function click(evt:MouseEvent):void {
 		var app:Scratch = root as Scratch;
 		if (!app) return;
-		var now:uint = getTimer();
+		var now:uint = CachedTimer.getCachedTimer();
 		app.runtime.startClickedHats(this);
 		if ((now - lastClickTime) < DOUBLE_CLICK_MSECS) {
 			if (isStage || ScratchSprite(this).isClone) return;
diff --git a/src/scratch/ScratchRuntime.as b/src/scratch/ScratchRuntime.as
index 51c4534..2d85b9a 100644
--- a/src/scratch/ScratchRuntime.as
+++ b/src/scratch/ScratchRuntime.as
@@ -108,7 +108,7 @@ public class ScratchRuntime {
 			return;
 		}
 		if (ready==ReadyLabel.COUNTDOWN) {
-			var tR:Number = getTimer()*.001-videoSeconds;
+			var tR:Number = CachedTimer.getCachedTimer()*.001-videoSeconds;
 			while (t>videoSounds.length/videoFramerate+1/videoFramerate) {
 				saveSound();
 			}
@@ -135,7 +135,7 @@ public class ScratchRuntime {
 			}
 		}
 		if (recording) { // Recording a YouTube video?
-			var t:Number = getTimer()*.001-videoSeconds;
+			var t:Number = CachedTimer.getCachedTimer()*.001-videoSeconds;
 			//If, based on time and framerate, the current frame needs to be in the video, capture the frame.
 			//Will always be true if framerate is 30, as every frame is captured.
 			if (t>videoSounds.length/videoFramerate+1/videoFramerate) {
@@ -213,7 +213,7 @@ public class ScratchRuntime {
 	
 	private function saveFrame():void {
 		saveSound();
-		var t:Number = getTimer()*.001-videoSeconds;
+		var t:Number = CachedTimer.getCachedTimer()*.001-videoSeconds;
 		while (t>videoSounds.length/videoFramerate+1/videoFramerate) {
 			saveSound();
 		}
@@ -380,7 +380,7 @@ public class ScratchRuntime {
 			videoHeight = 360;
 		}
 		ready=ReadyLabel.COUNTDOWN;
-		videoSeconds = getTimer()*.001;
+		videoSeconds = CachedTimer.getCachedTimer()*.001;
 		baFlvEncoder = new ByteArrayFlvEncoder(videoFramerate);
 		baFlvEncoder.setVideoProperties(videoWidth, videoHeight);
 		baFlvEncoder.setAudioProperties(FlvEncoder.SAMPLERATE_44KHZ, true, true, true);
@@ -433,7 +433,7 @@ public class ScratchRuntime {
 		ready=ReadyLabel.NOT_READY;
 		app.refreshStagePart();
 		var player:ScratchSoundPlayer, length:int;
-		videoSeconds = getTimer() * 0.001;
+		videoSeconds = CachedTimer.getCachedTimer() * 0.001;
 		for each (player in ScratchSoundPlayer.activeSounds) {
 			length = int((player.soundChannel.position*.001)*videoFramerate);
 			player.readPosition = Math.max(Math.min(baFlvEncoder.audioFrameSize*length,player.dataBytes.length),0);
diff --git a/src/sound/ScratchSoundPlayer.as b/src/sound/ScratchSoundPlayer.as
index b0573f9..5a466c0 100644
--- a/src/sound/ScratchSoundPlayer.as
+++ b/src/sound/ScratchSoundPlayer.as
@@ -33,6 +33,8 @@ import flash.utils.ByteArray;
 
 import scratch.ScratchSound;
 
+import util.CachedTimer;
+
 public class ScratchSoundPlayer {
 
 	static public var activeSounds:Array = [];
@@ -160,7 +162,7 @@ public class ScratchSoundPlayer {
 
 	private function writeSampleData(evt:SampleDataEvent):void {
 		var i:int;
-		if ((lastBufferTime != 0) && ((getTimer() - lastBufferTime) > 230)) {
+		if ((lastBufferTime != 0) && ((CachedTimer.getCachedTimer() - lastBufferTime) > 230)) {
 			soundChannel = null; // don't explicitly stop the sound channel in this callback; allow it to stop on its own
 			stopPlaying();
 			return;
@@ -174,7 +176,7 @@ public class ScratchSoundPlayer {
 		}
 		dataBytes.writeBytes(data);
 		if ((bytePosition >= endOffset) && (lastBufferTime == 0)) {
-			lastBufferTime = getTimer();
+			lastBufferTime = CachedTimer.getCachedTimer();
 		}
 	}
 
diff --git a/src/ui/parts/LibraryPart.as b/src/ui/parts/LibraryPart.as
index 9961bf3..aa29e4d 100644
--- a/src/ui/parts/LibraryPart.as
+++ b/src/ui/parts/LibraryPart.as
@@ -32,6 +32,8 @@ import ui.media.*;
 import ui.SpriteThumbnail;
 import uiwidgets.*;
 
+import util.CachedTimer;
+
 public class LibraryPart extends UIPart {
 
 	private const smallTextFormat:TextFormat = new TextFormat(CSS.font, 10, CSS.textColor);
@@ -251,12 +253,12 @@ public class LibraryPart extends UIPart {
 	public function step():void {
 		// Update thumbnails and sprite details.
 		var viewedObj:ScratchObj = app.viewedObj();
-		var updateThumbnails:Boolean = ((getTimer() - lastUpdate) > updateInterval);
+		var updateThumbnails:Boolean = ((CachedTimer.getCachedTimer() - lastUpdate) > updateInterval);
 		for each (var tn:SpriteThumbnail in allThumbnails()) {
 			if (updateThumbnails) tn.updateThumbnail();
 			tn.select(tn.targetObj == viewedObj);
 		}
-		if (updateThumbnails) lastUpdate = getTimer();
+		if (updateThumbnails) lastUpdate = CachedTimer.getCachedTimer();
 		if (spriteDetails.visible) spriteDetails.step();
 		if (videoButton && videoButton.visible) updateVideoButton();
 	}
diff --git a/src/ui/parts/ScriptsPart.as b/src/ui/parts/ScriptsPart.as
index cf707f1..7e621f9 100644
--- a/src/ui/parts/ScriptsPart.as
+++ b/src/ui/parts/ScriptsPart.as
@@ -33,6 +33,8 @@ import ui.*;
 
 import uiwidgets.*;
 
+import util.CachedTimer;
+
 public class ScriptsPart extends UIPart {
 
 	private var shape:Shape;
@@ -130,12 +132,12 @@ public class ScriptsPart extends UIPart {
 	private var lastUpdateTime:uint;
 
 	private function updateExtensionIndicators():void {
-		if ((getTimer() - lastUpdateTime) < 500) return;
+		if ((CachedTimer.getCachedTimer() - lastUpdateTime) < 500) return;
 		for (var i:int = 0; i < app.palette.numChildren; i++) {
 			var indicator:IndicatorLight = app.palette.getChildAt(i) as IndicatorLight;
 			if (indicator) app.extensionManager.updateIndicator(indicator, indicator.target);
 		}		
-		lastUpdateTime = getTimer();
+		lastUpdateTime = CachedTimer.getCachedTimer();
 	}
 
 	public function setWidthHeight(w:int, h:int):void {
diff --git a/src/uiwidgets/Menu.as b/src/uiwidgets/Menu.as
index 2069dea..418fde2 100644
--- a/src/uiwidgets/Menu.as
+++ b/src/uiwidgets/Menu.as
@@ -34,6 +34,8 @@ package uiwidgets {
 	import flash.utils.getTimer;
 	import translation.TranslatableStrings;
 
+import util.CachedTimer;
+
 public class Menu extends Sprite {
 
 	// when stringCollectionMode is true menus are not displayed but strings are recorded for translation
@@ -192,8 +194,8 @@ public class Menu extends Sprite {
 			return;
 		}
 
-		if ((getTimer() - lastTime) < scrollMSecs) return;
-		lastTime = getTimer();
+		if ((CachedTimer.getCachedTimer() - lastTime) < scrollMSecs) return;
+		lastTime = CachedTimer.getCachedTimer();
 
 		var localY:int = this.globalToLocal(new Point(stage.mouseX, stage.mouseY)).y;
 		if ((localY < (2 + scrollInset)) && (firstItemIndex > 0)) scrollBy(-1);
diff --git a/src/util/CachedTimer.as b/src/util/CachedTimer.as
new file mode 100644
index 0000000..fd41811
--- /dev/null
+++ b/src/util/CachedTimer.as
@@ -0,0 +1,55 @@
+/*
+ * Scratch Project Editor and Player
+ * Copyright (C) 2018 Massachusetts Institute of Technology
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package util {
+import flash.utils.getTimer;
+
+/**
+ * Calling getTimer() is much more expensive in Flash 30 than in previous versions.
+ * This class is meant to reduce the number of actual calls to getTimer() with minimal changes to existing code.
+ */
+public class CachedTimer {
+	private static var dirty:Boolean = true;
+	private static var cachedTimer:int;
+
+	/**
+	 * @return the last cached value of getTimer(). May return a fresh value if the cache has been invalidated.
+	 */
+	public static function getCachedTimer():int {
+		return dirty ? getFreshTimer() : cachedTimer;
+	}
+
+	/**
+	 * Clear the timer cache, forcing getCachedTimer() to get a fresh value next time. Use this at the top of a frame.
+	 */
+	public static function clearCachedTimer():void {
+		dirty = true;
+	}
+
+	/**
+	 * @return and cache the current value of getTimer().
+	 * Use this if you need an accurate timer value in the middle of a frame.
+	 */
+	public static function getFreshTimer():int {
+		cachedTimer = getTimer();
+		dirty = false;
+		return cachedTimer;
+	}
+}
+}
diff --git a/src/util/GestureHandler.as b/src/util/GestureHandler.as
index 8b95ab5..e5526a0 100644
--- a/src/util/GestureHandler.as
+++ b/src/util/GestureHandler.as
@@ -123,7 +123,7 @@ public class GestureHandler {
 	}
 
 	public function step():void {
-		if ((getTimer() - mouseDownTime) > DOUBLE_CLICK_MSECS) {
+		if ((CachedTimer.getCachedTimer() - mouseDownTime) > DOUBLE_CLICK_MSECS) {
 			if (gesture == "unknown") {
 				if (mouseTarget != null) handleDrag(null);
 				if (gesture != 'drag') handleClick(mouseDownEvent);
@@ -132,7 +132,7 @@ public class GestureHandler {
 				handleClick(mouseDownEvent);
 			}
 		}
-		if (carriedObj && scrollTarget && (getTimer() - scrollStartTime) > SCROLL_MSECS && (scrollXVelocity || scrollYVelocity)) {
+		if (carriedObj && scrollTarget && (CachedTimer.getCachedTimer() - scrollStartTime) > SCROLL_MSECS && (scrollXVelocity || scrollYVelocity)) {
 			if (scrollTarget.allowHorizontalScrollbar) {
 				scrollTarget.contents.x = Math.min(0, Math.max(-scrollTarget.maxScrollH(), scrollTarget.contents.x + scrollXVelocity));
 			}
@@ -195,7 +195,7 @@ public class GestureHandler {
 			handleTool(evt);
 			return;
 		}
-		mouseDownTime = getTimer();
+		mouseDownTime = CachedTimer.getCachedTimer();
 		mouseDownEvent = evt;
 		gesture = "unknown";
 		mouseTarget = null;
@@ -261,7 +261,7 @@ public class GestureHandler {
 			if (t is ScrollFrameContents) {
 				scrollTarget = t.parent as ScrollFrame;
 				if (scrollTarget != oldTarget) {
-					scrollStartTime = getTimer();
+					scrollStartTime = CachedTimer.getCachedTimer();
 				}
 				break;
 			}
@@ -293,7 +293,7 @@ public class GestureHandler {
 				}
 			}
 			if (!scrollXVelocity && !scrollYVelocity) {
-				scrollStartTime = getTimer();
+				scrollStartTime = CachedTimer.getCachedTimer();
 			}
 		}
 		if (bubble) {
@@ -540,7 +540,7 @@ public class GestureHandler {
 		obj.startDrag();
 		if(obj is DisplayObject) obj.cacheAsBitmap = true;
 		carriedObj = obj;
-		scrollStartTime = getTimer();
+		scrollStartTime = CachedTimer.getCachedTimer();
 	}
 
 	private function dropHandled(droppedObj:*, evt:MouseEvent):Boolean {
diff --git a/src/util/Perf.as b/src/util/Perf.as
deleted file mode 100644
index 27fb553..0000000
--- a/src/util/Perf.as
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Scratch Project Editor and Player
- * Copyright (C) 2014 Massachusetts Institute of Technology
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-
-package util {
-import flash.utils.getTimer;
-
-public class Perf {
-
-	private static var totalStart:uint;
-	private static var lapStart:uint;
-	private static var lapTotal:uint;
-
-	public static function start(msg:String = null):void {
-		if (!msg) msg = 'Perf.start';
-		Scratch.app.log(msg);
-		totalStart = lapStart = getTimer();
-		lapTotal = 0;
-	}
-
-	public static function clearLap():void {
-		lapStart = getTimer();
-	}
-
-	public static function lap(msg:String = ""):void {
-		if (totalStart == 0) return; // not monitoring performance
-		var lapMSecs:uint = getTimer() - lapStart;
-		Scratch.app.log('  ' + msg + ': ' + lapMSecs + ' msecs');
-		lapTotal += lapMSecs;
-		lapStart = getTimer();
-	}
-
-	public static function end():void {
-		if (totalStart == 0) return; // not monitoring performance
-		var totalMSecs:uint = getTimer() - totalStart;
-		var unaccountedFor:uint = totalMSecs - lapTotal;
-		Scratch.app.log('Total: ' + totalMSecs + ' msecs; unaccounted for: ' + unaccountedFor + ' msecs (' + int((100 * unaccountedFor) / totalMSecs) + '%)');
-		totalStart = lapStart = lapTotal = 0;
-	}
-}}
diff --git a/src/util/Transition.as b/src/util/Transition.as
index 408ff36..bbe3edc 100644
--- a/src/util/Transition.as
+++ b/src/util/Transition.as
@@ -48,7 +48,7 @@ public class Transition {
 		} else {
 			delta = endValue - startValue;
 		}
-		startMSecs = getTimer();
+		startMSecs = CachedTimer.getCachedTimer();
 		duration = 1000 * secs;
 	}
 
@@ -66,7 +66,7 @@ public class Transition {
 
 	public static function step(evt:*):void {
 		if (activeTransitions.length == 0) return;
-		var now:uint = getTimer();
+		var now:uint = CachedTimer.getCachedTimer();
 		var newActive:Array = [];
 		for each (var t:Transition in activeTransitions) {
 			 if (t.apply(now)) newActive.push(t);
diff --git a/src/watchers/ListWatcher.as b/src/watchers/ListWatcher.as
index 4ca4f92..7bb4af0 100644
--- a/src/watchers/ListWatcher.as
+++ b/src/watchers/ListWatcher.as
@@ -26,7 +26,9 @@ package watchers {
 	import interpreter.Interpreter;
 	import scratch.ScratchObj;
 	import translation.Translator;
-	import util.JSON;
+
+import util.CachedTimer;
+import util.JSON;
 	import uiwidgets.*;
 
 public class ListWatcher extends Sprite {
@@ -222,7 +224,7 @@ public class ListWatcher extends Sprite {
 		if (!visible) return;
 		adjustLastAccessSize();
 		if ((i < 1) || (i > lastAccess.length)) return;
-		lastAccess[i - 1] = getTimer();
+		lastAccess[i - 1] = CachedTimer.getCachedTimer();
 		lastActiveIndex = i - 1;
 		interp.redraw();
 	}
@@ -265,7 +267,7 @@ public class ListWatcher extends Sprite {
 		// Highlight the cell number of all recently accessed cells currently visible.
 		const fadeoutMSecs:int = 800;
 		adjustLastAccessSize();
-		var now:int = getTimer();
+		var now:int = CachedTimer.getCachedTimer();
 		isIdle = true; // try to be idle; set to false if any non-zero lastAccess value is found
 		for (var i:int = 0; i < visibleCellNums.length; i++) {
 			var lastAccessTime:int = lastAccess[firstVisibleIndex + i];

From b0de4bb377d8586dfff7ac22b7468c7a3a8f5f5c Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <cwillisf@media.mit.edu>
Date: Fri, 8 Jun 2018 15:29:23 -0700
Subject: [PATCH 2/4] Use cached timer more often when warping

---
 src/interpreter/Interpreter.as | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/interpreter/Interpreter.as b/src/interpreter/Interpreter.as
index e27435c..791a65e 100644
--- a/src/interpreter/Interpreter.as
+++ b/src/interpreter/Interpreter.as
@@ -224,7 +224,7 @@ public class Interpreter {
 	public function stepThreads():void {
 		var workTime:int = (0.75 * 1000) / app.stage.frameRate; // work for up to 75% of one frame time
 		doRedraw = false;
-		startTime = currentMSecs = CachedTimer.getCachedTimer();
+		startTime = currentMSecs = CachedTimer.getFreshTimer();
 		if (threads.length == 0) return;
 		while ((currentMSecs - startTime) < workTime) {
 			if (warpThread && (warpThread.block == null)) clearWarpBlock();
@@ -265,13 +265,15 @@ public class Interpreter {
 			}
 		}
 		yield = false;
+		var warpStartTimer:int = CachedTimer.getCachedTimer();
 		while (true) {
-			if (activeThread == warpThread) currentMSecs = CachedTimer.getFreshTimer();
+			if (activeThread == warpThread) currentMSecs = warpStartTimer;
 			evalCmd(activeThread.block);
 			if (yield) {
 				if (activeThread == warpThread) {
 					if ((currentMSecs - startTime) > warpMSecs) return;
 					yield = false;
+					warpStartTimer = CachedTimer.getFreshTimer();
 					continue;
 				} else return;
 			}

From 0c605cabd3c511457108d08215cedb4c53581990 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <cwillisf@media.mit.edu>
Date: Fri, 8 Jun 2018 17:05:54 -0700
Subject: [PATCH 3/4] Minimize how often we check against workTime

---
 src/interpreter/Interpreter.as | 35 ++++++++++++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)

diff --git a/src/interpreter/Interpreter.as b/src/interpreter/Interpreter.as
index 791a65e..a515d8f 100644
--- a/src/interpreter/Interpreter.as
+++ b/src/interpreter/Interpreter.as
@@ -62,7 +62,6 @@ import extensions.ExtensionManager;
 
 import flash.geom.Point;
 import flash.utils.Dictionary;
-import flash.utils.getTimer;
 
 import primitives.*;
 
@@ -221,11 +220,34 @@ public class Interpreter {
 		doRedraw = true;
 	}
 
+	private const workTimeCheckIntervalFactor:Number = 1/3.0;
+	private const maxIterationCountSamples: uint = 10;
+	private var iterationCountSamples: Vector.<uint> = new <uint>[500]; // initial guess
+
+	private function addIterationCountSample(sample:uint):void {
+		iterationCountSamples.push(sample);
+		while (iterationCountSamples.length > maxIterationCountSamples) {
+			iterationCountSamples.shift();
+		}
+	}
+
+	private function getAverageIterationCount():Number {
+		var total:uint = 0;
+		for each (var sample:uint in iterationCountSamples) {
+			total += sample;
+		}
+		return Number(total) / iterationCountSamples.length;
+	}
+
 	public function stepThreads():void {
 		var workTime:int = (0.75 * 1000) / app.stage.frameRate; // work for up to 75% of one frame time
 		doRedraw = false;
 		startTime = currentMSecs = CachedTimer.getFreshTimer();
 		if (threads.length == 0) return;
+		var currentEstimate:Number = getAverageIterationCount();
+		var iterationCount:uint = 0;
+		var checkInterval:uint = Math.round(workTimeCheckIntervalFactor * currentEstimate);
+		var checkCount:uint = 0;
 		while ((currentMSecs - startTime) < workTime) {
 			if (warpThread && (warpThread.block == null)) clearWarpBlock();
 			var threadStopped:Boolean = false;
@@ -248,9 +270,18 @@ public class Interpreter {
 				threads = newThreads;
 				if (threads.length == 0) return;
 			}
-			currentMSecs = CachedTimer.getFreshTimer();
 			if (doRedraw || (runnableCount == 0)) return;
+			++iterationCount;
+			++checkCount;
+			if (checkCount >= checkInterval) {
+				currentMSecs = CachedTimer.getFreshTimer();
+				checkCount = 0;
+			}
 		}
+		// if we get here, this was a frame where we needed to check the timer twice or more
+		// use the elapsed time and actual iteration count to generate an estimate for iterations per step
+		var newEstimate:uint = Math.round(workTime * iterationCount / Number(currentMSecs - startTime));
+		addIterationCountSample(newEstimate);
 	}
 
 	private function stepActiveThread():void {

From 825cbcc0cbab3f1e7f801a68ad77419ea73ed9e3 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <cwillisf@media.mit.edu>
Date: Mon, 11 Jun 2018 10:26:45 -0700
Subject: [PATCH 4/4] v461 version bump: Flash 30 performance changes

---
 src/Scratch.as | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Scratch.as b/src/Scratch.as
index b8bd666..b6947ca 100644
--- a/src/Scratch.as
+++ b/src/Scratch.as
@@ -75,7 +75,7 @@ import watchers.ListWatcher;
 
 public class Scratch extends Sprite {
 	// Version
-	public static const versionString:String = 'v460.0.1';
+	public static const versionString:String = 'v461';
 	public static var app:Scratch; // static reference to the app, used for debugging
 
 	// Display modes