Increasing lag by the end of the game.
Multiple WFTDA tournaments have reported that the clocks have been lagging to a greater and greater degree over the course of a game.  By the end of the second half, lag can accumulate up to as much as several seconds.  This is obviously a critical issue for a piece of timing software.  I'll attach reports below from several tournaments.

It appears that throwing CPU speed at the problem is presently the only thing that improves the situation.

### Udder Chaos

Running CRG on an i5 laptop, scoreboard operator is on the same computer.  This computer is dedicated to CRG - it has nothing else installed (running Win 10).

The overlay computer and the scoreboard projection computer are both remote and show lag.

Also have 4 computers showing the electronic whiteboard.  

Restarting CRG before each game, clearing the chrome cache at the same time as the restart.

The CRG scoreboard is lagging.
It doesn't lose time on the game clock, but it doesn't start the lineup clock until after it finishes lagging.

This is adding from 2-11 seconds to lineup times.

-

First half nearly over with CRG on Linux Mint, issue is still present. What we are seeing that is the most concerning is that after a jam ends, it takes progressively longer for the lineup clock to start. We've been keeping track, and it's between 45 second and 2 minutes of game time that is run off the clock that shouldn't be - this could definitely have an effect on the outcome of a game. 

Previous game we tried only loading in the two teams we were using, no change in the behavior.

-

The last game on Windows and this game on Linux both lost 45 seconds of game time due to the lineup clock not starting - so Mint definitely isn't the magic bullet.

-

For our first game today we used a Linux Mint live USB to boot a core i7 box with 24 gb ram (retail price US$1899 for this machine). Things were pretty good until the 20 to 25 minute mark of the second half, where we started losing about 2 seconds after each jam for 75% of jams. 

Still not great, but at least we're not losing 2 minutes a game.

Also have projector, overlay, and operators computers all running separately (plus the 3 whiteboards).

-

One more note, on the i7 box we are seeing CPU usage spike to ~60% as a jam ends, when the lineup clock is supposed to start (on the core2 duo we tried first it would spike to 90%). After the CPU usage drops back down, the clocks are good.

### Euroclash

Setup: CRG 3.9.3a running on Linux Mint 18.3, i7 8gb ram laptop, connected by ethernet to the private network, and to the projector. There are digital whiteboard displays in both team benches, the penalty box, THOs table, and announcer table.

Issues:
 - I had similar issues with the digital whiteboards as Jon reported at the Big O, spaces on the display where penalties weren't; penalties disappearing; and something happened to the system whilst I was on a break, that "fixed itself" and no one will give me specifics about!
 - Lag: the lag curve was constant throughout the day, starting out unnoticeable and exponentially getting more laggy towards the middle of the second period where it seems to level out at around 2 seconds of lag. THOs and crew heads happy that this is an acceptable amount of lag.
 ○ Steps taken to deal with the lag included going to the JSON scoreboard display which has a bug where banner ads aren't being displayed (no change); and restarting the entire scoreboard server machine before the final game of the day (big change). I am planning on including this in the order of operations before starting a new game (current procedure being at the end of the game to kill the app, restart it and then use "Start New Game")


From personal testing the load is on the cpu, not necessarily the ram capacity (but more cannot hurt!)

diff --git a/html/controls/pt/index.js b/html/controls/pt/index.js
index 478057b4..868d5002 100644
--- a/html/controls/pt/index.js
+++ b/html/controls/pt/index.js
@@ -18,8 +18,8 @@ function initialize() {
 	WS.Register( [ 'ScoreBoard.Clock(Period).Number' ], function(k, v) { period = v; });
 	WS.Register( [ 'ScoreBoard.Clock(Jam).Number' ], function(k, v) { jam = v; });
 
-	WS.Register( [ 'Game.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } );
-	WS.Register( [ 'Game.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } );
+	WS.Register( [ 'ScoreBoard.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } );
+	WS.Register( [ 'ScoreBoard.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } );
 	WS.Register( [ 'PenaltyCode' ], function(k, v) { penaltyCode(k, v); } );
 	WS.Register( [ 'ScoreBoard.Clock(Period).MinimumNumber', 'ScoreBoard.Clock(Period).MaximumNumber' ], function(k, v) { setupSelect('Period'); } );
 	WS.Register( [ 'ScoreBoard.Clock(Jam).MinimumNumber', 'ScoreBoard.Clock(Jam).MaximumNumber' ], function(k, v) { setupSelect('Jam'); } );
@@ -64,7 +64,7 @@ function skaterUpdate(t, k, v) {
 	if (match == null || match.length == 0)
 		return;
 	var id = match[1];
-	var prefix = 'Game.Team(' + t + ').Skater(' + id + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';
 	if (k == prefix + '.Number') {
 		var row = $('.Team' + t + ' .Skater.Penalty[id=' + id + ']');
 		if (v == null) {
@@ -93,7 +93,7 @@ function displayPenalty(t, s, p) {
 	var jamBox = $('.Team' + t + ' .Skater.Jam[id=' + s + '] .Box' + p);
 	var totalBox = $('.Team' + t + ' .Skater.Penalty[id=' + s + '] .Total');
 
-	var prefix = 'Game.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
 	code = WS.state[prefix + ".Code"];
 
 	if (code != null) {
@@ -165,7 +165,7 @@ function openPenaltyEditor(t, id, which) {
 	if (teamColor == null)
 		teamColor = WS.state[prefix + '.Name'];
 
-	prefix = 'Game.Team(' + t + ').Skater(' + id + ')';
+	prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';
 	var skaterName = WS.state[prefix + '.Name'];
 	var skaterNumber = WS.state[prefix + '.Number'];
 	teamId = t;
@@ -254,4 +254,4 @@ function penaltyCode(k, v) {
 		$('<div>').text(d).appendTo(desc);
 	});
 }
-//# sourceURL=controls\pt\index.js
\ No newline at end of file
+//# sourceURL=controls\pt\index.js
diff --git a/html/controls/pt/ptcolor.js b/html/controls/pt/ptcolor.js
index 81f8d473..645bb605 100755
--- a/html/controls/pt/ptcolor.js
+++ b/html/controls/pt/ptcolor.js
@@ -23,8 +23,8 @@ function initialize() {
 	WS.Register( [ 'ScoreBoard.Clock(Period).Number' ], function(k, v) { period = v; });
 	WS.Register( [ 'ScoreBoard.Clock(Jam).Number' ], function(k, v) { jam = v; });
 
-	WS.Register( [ 'Game.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } );
-	WS.Register( [ 'Game.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } );
+	WS.Register( [ 'ScoreBoard.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } );
+	WS.Register( [ 'ScoreBoard.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } );
 	WS.Register( [ 'PenaltyCode' ], function(k, v) { penaltyCode(k, v); } );
 	WS.Register( [ 'ScoreBoard.Clock(Period).MinimumNumber', 'ScoreBoard.Clock(Period).MaximumNumber' ], function(k, v) { setupSelect('Period'); } );
 	WS.Register( [ 'ScoreBoard.Clock(Jam).MinimumNumber', 'ScoreBoard.Clock(Jam).MaximumNumber' ], function(k, v) { setupSelect('Jam'); } );
@@ -69,7 +69,7 @@ function skaterUpdate(t, k, v) {
 	if (match == null || match.length == 0)
 		return;
 	var id = match[1];
-	var prefix = 'Game.Team(' + t + ').Skater(' + id + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';
 	if (k == prefix + '.Number') {
 		var row = $('.Team' + t + ' .Skater.Penalty[id=' + id + ']');
 		if (v == null) {
@@ -98,7 +98,7 @@ function displayPenalty(t, s, p) {
 	var jamBox = $('.Team' + t + ' .Skater.Jam[id=' + s + '] .Box' + p);
 	var totalBox = $('.Team' + t + ' .Skater.Penalty[id=' + s + '] .Total');
 
-	var prefix = 'Game.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
 	code = WS.state[prefix + ".Code"];
 
 	if (code != null) {
@@ -178,7 +178,7 @@ function openPenaltyEditor(t, id, which) {
 	if (teamColor == null)
 		teamColor = WS.state[prefix + '.Name'];
 
-	prefix = 'Game.Team(' + t + ').Skater(' + id + ')';
+	prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';
 	var skaterName = WS.state[prefix + '.Name'];
 	var skaterNumber = WS.state[prefix + '.Number'];
 	teamId = t;
diff --git a/html/views/overlays/ddtv/admin.js b/html/views/overlays/ddtv/admin.js
index 75b45826..1113a4b5 100755
--- a/html/views/overlays/ddtv/admin.js
+++ b/html/views/overlays/ddtv/admin.js
@@ -57,8 +57,8 @@ function initialize() {
 		});
 	});
 
-	var skaterRegEx = /^Game\.Team\((.+)\)\.Skater\((.+?)\)\.(.+)$/;
-	WS.Register('Game.Team', function(k,v) {
+	var skaterRegEx = /^ScoreBoard\.Team\((.+)\)\.Skater\((.+?)\)\.(.+)$/;
+	WS.Register('ScoreBoard.Team', function(k,v) {
 		var m = k.match(skaterRegEx);
 		if(m) {
 			var key = m[3];
@@ -119,8 +119,8 @@ $('select#Skaters').change(function(e) {
 	v = $t.val();
 	team = $( 'option[value=' + v + ']', $t ).attr('data-team');
 	name = $( 'option[value=' + v + ']', $t ).attr('data-name');
-	tnam = WS.state['Game.Team(' + team + ').AlternateName(overlay)'];
-	tnam = tnam ? tnam : WS.state['Game.Team(' + team + ').Name'];
+	tnam = WS.state['ScoreBoard.Team(' + team + ').AlternateName(overlay)'];
+	tnam = tnam ? tnam : WS.state['ScoreBoard.Team(' + team + ').Name'];
 	f = $( '#LowerThirdStyle option[value=ColourTeam' + team + ']').attr('selected', 'selected').change();
 	$('input[data-setting="Custom.Overlay.LowerThird.Line1"]').val(name).change();
 	$('input[data-setting="Custom.Overlay.LowerThird.Line2"]').val(tnam).change();
diff --git a/html/views/overlays/ddtv/index.js b/html/views/overlays/ddtv/index.js
index 277af18b..5cedeb7a 100755
--- a/html/views/overlays/ddtv/index.js
+++ b/html/views/overlays/ddtv/index.js
@@ -70,8 +70,8 @@ function initialize() {
 
 	WS.Register( 'Game.Period', function(k,v) { jamData(k,v); } );
 
-	WS.Register( 'Game.Team(1)', function(k,v) { teamData(1, k,v); } );
-	WS.Register( 'Game.Team(2)', function(k,v) { teamData(2, k,v); } );
+	WS.Register( 'ScoreBoard.Team(1)', function(k,v) { teamData(1, k,v); } );
+	WS.Register( 'ScoreBoard.Team(2)', function(k,v) { teamData(2, k,v); } );
 
 	WS.Register( 'ScoreBoard.Clock(Period).Number', function(k,v) {
 		if(v == 2) { $('.PPJBox .Team .Period2').show(); } else { $('.PPJBox .Team .Period2').hide(); }
@@ -358,8 +358,8 @@ function pointsPerJamColumnWidths() {
 	wid = parseInt( $('.PPJBox').innerWidth() );
 	newwidth = parseInt(wid / nel) - 3;
 	$('.ColumnWidth').innerWidth(newwidth);
-	$('.PPJBox .Team1 .GraphBlock').css('backgroundColor', WS.state['Game.Team(1).Color(overlay_bg)']);
-	$('.PPJBox .Team2 .GraphBlock').css('backgroundColor', WS.state['Game.Team(2).Color(overlay_bg)']);
+	$('.PPJBox .Team1 .GraphBlock').css('backgroundColor', WS.state['ScoreBoard.Team(1).Color(overlay_bg)']);
+	$('.PPJBox .Team2 .GraphBlock').css('backgroundColor', WS.state['ScoreBoard.Team(2).Color(overlay_bg)']);
 }
 
 function clockType(k,v) {
diff --git a/html/views/overlays/interactive/index.js b/html/views/overlays/interactive/index.js
index 1eeaf11e..fbc7ba64 100755
--- a/html/views/overlays/interactive/index.js
+++ b/html/views/overlays/interactive/index.js
@@ -60,8 +60,8 @@ function initialize() {
 
 	WS.Register( 'Game.Period', function(k,v) { jamData(k,v); } );
 
-	WS.Register( 'Game.Team(1)', function(k,v) { teamData(1, k,v); } );
-	WS.Register( 'Game.Team(2)', function(k,v) { teamData(2, k,v); } );
+	WS.Register( 'ScoreBoard.Team(1)', function(k,v) { teamData(1, k,v); } );
+	WS.Register( 'ScoreBoard.Team(2)', function(k,v) { teamData(2, k,v); } );
 
 	WS.Register( 'ScoreBoard.Clock(Period).Number', function(k,v) {
 		if(v == 2) { $('.PPJBox .Team .Period2').show(); } else { $('.PPJBox .Team .Period2').hide(); }
@@ -125,17 +125,17 @@ function teamData(team, k,v) {
 	var key;
 	var setting; 
 
-	var skaterRegEx = /^Game\.Team\((.+)\)\.Skater\((.+)\)\.(.+)$/;
+	var skaterRegEx = /^ScoreBoard\.Team\((.+)\)\.Skater\((.+)\)\.(.+)$/;
 	var match = k.match(skaterRegEx);
 	if(match) { 
 		skaterId = match[2]; key = match[3]; penalty = null; 
 	}
 
-	var penaltyRegEx = /^Game\.Team\((.+)\)\.Skater\((.+)\)\.Penalty\((.+)\).(.+)$/;
+	var penaltyRegEx = /^ScoreBoard\.Team\((.+)\)\.Skater\((.+)\)\.Penalty\((.+)\).(.+)$/;
 	var match = k.match(penaltyRegEx);
 	if(match) { skaterId = match[2]; key = match[4]; penalty = match[3]; }
 
-	var teamInfoRegEx = /^Game\.Team\((.+)\)\.(.+)$/;
+	var teamInfoRegEx = /^ScoreBoard\.Team\((.+)\)\.(.+)$/;
 	var match = k.match(teamInfoRegEx);
 	if(match) { 
 		var subkey = match[2];
@@ -150,7 +150,7 @@ function teamData(team, k,v) {
 		}
 	}
 
-	var colourRegEx = /^Game\.Team\((.+)\)\.Color\((.+)\)$/;
+	var colourRegEx = /^ScoreBoard\.Team\((.+)\)\.Color\((.+)\)$/;
 	var match = k.match(colourRegEx);
 	if(match) { 
 		var setting = match[2];
diff --git a/html/views/overlays/wb/tcdg2016wb.js b/html/views/overlays/wb/tcdg2016wb.js
index 72b9a5fa..cd8b0024 100755
--- a/html/views/overlays/wb/tcdg2016wb.js
+++ b/html/views/overlays/wb/tcdg2016wb.js
@@ -41,8 +41,8 @@ function initialize() {
         WS.Register( [ 'ScoreBoard.Team(1).Logo' ], function(k, v) { $('.Logo1').attr('src', v); } );
         WS.Register( [ 'ScoreBoard.Team(2).Logo' ], function(k, v) { $('.Logo2').attr('src', v); } );
 
-	WS.Register( [ 'Game.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
-	WS.Register( [ 'Game.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
+	WS.Register( [ 'ScoreBoard.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
+	WS.Register( [ 'ScoreBoard.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
 
 }
 
@@ -54,8 +54,8 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 	if (match == null || match.length == 0)
 		return;
 	var id = match[1]; // id = skater id
-	var prefix = 'Game.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = Game.Team('1').Skater('id')
-	if (k == prefix + '.Number') { //Example if skater id == Game.Team('team').Skater('id').Number
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = Game.Team('1').Skater('id')
+	if (k == prefix + '.Number') { //Example if skater id == ScoreBoard.Team('team').Skater('id').Number
 		var rowd = $('.Teamd' + t + ' .Skater.Penalty[id=' + id + ']');
 		if (v == null) { // if jam number is null
 			$('.Teamd' + t + ' .Skater[id=' + id + ']').remove();
@@ -68,7 +68,7 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 		for (var i = 1; i <= 9; i++) { // for penalty numbers one to nine..
 			displayPenalty(t, id, i); } // display penalties (team, skater id, penalty #)
 		displayPenalty(t, id, 'FO_EXP'); // display foulout status
-	} else {  // if skater id does NOT match Game.Team('team').Skater('id').Number
+	} else {  // if skater id does NOT match ScoreBoard.Team('team').Skater('id').Number
 		// Look for penalty
 		match = k.match(penaltyRegex);
 		if (match == null || match.length == 0)
@@ -82,8 +82,8 @@ function displayPenalty(t, s, p) { // team skater penalty#
 	var penaltyBoxd = $('.Teamd' + t + ' .Skater.Penalty[id=' + s + '] .Box' + p);
 	var totalBoxd = $('.Teamd' + t + ' .Skater.Penalty[id=' + s + '] .Total');
 
-	var prefix = 'Game.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
-	var nprefix = 'Game.Team(' + t + ').Skater(' + s + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
+	var nprefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ')';
 	code = WS.state[prefix + ".Code"];
 	if (code != null) {
 		penaltyBoxd.data("id", WS.state[prefix + ".Id"]);
@@ -123,7 +123,7 @@ function makeSkaterRows(t, id, number) { //team, id, number
         $('.Team' + t + 'custColor').css('background-color', teamBColor);
 	
 	pd.append($('<div width=10%>').addClass('Number').text(number));
-	pd.append($('<div width=50%>').addClass('Name').text(WS.state['Game.Team(' + t + ').Skater(' + id + ').Name']));
+	pd.append($('<div width=50%>').addClass('Name').text(WS.state['ScoreBoard.Team(' + t + ').Skater(' + id + ').Name']));
 	$.each([1, 2, 3, 4, 5, 6, 7, 8, 9], function(idx, c) {
 		pd.append($('<div width=10%>').addClass('Box Box' + c).html('&nbsp;'));
 	});
diff --git a/html/views/overlays/wb/tcdg2016wb_underlay.js b/html/views/overlays/wb/tcdg2016wb_underlay.js
index ccbb558f..a73700f2 100755
--- a/html/views/overlays/wb/tcdg2016wb_underlay.js
+++ b/html/views/overlays/wb/tcdg2016wb_underlay.js
@@ -41,8 +41,8 @@ function initialize() {
         WS.Register( [ 'ScoreBoard.Team(1).Logo' ], function(k, v) { $('.Logo1').attr('src', v); } );
         WS.Register( [ 'ScoreBoard.Team(2).Logo' ], function(k, v) { $('.Logo2').attr('src', v); } );
 
-	WS.Register( [ 'Game.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
-	WS.Register( [ 'Game.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
+	WS.Register( [ 'ScoreBoard.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
+	WS.Register( [ 'ScoreBoard.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
 
 }
 
@@ -54,8 +54,8 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 	if (match == null || match.length == 0)
 		return;
 	var id = match[1]; // id = skater id
-	var prefix = 'Game.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = Game.Team('1').Skater('id')
-	if (k == prefix + '.Number') { //Example if skater id == Game.Team('team').Skater('id').Number
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = Game.Team('1').Skater('id')
+	if (k == prefix + '.Number') { //Example if skater id == ScoreBoard.Team('team').Skater('id').Number
 		var rowd = $('.Teamd' + t + ' .Skater.Penalty[id=' + id + ']');
 		if (v == null) { // if jam number is null
 			$('.Teamd' + t + ' .Skater[id=' + id + ']').remove();
@@ -68,7 +68,7 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 		for (var i = 1; i <= 9; i++) { // for penalty numbers one to nine..
 			displayPenalty(t, id, i); } // display penalties (team, skater id, penalty #)
 		displayPenalty(t, id, 'FO_EXP'); // display foulout status
-	} else {  // if skater id does NOT match Game.Team('team').Skater('id').Number
+	} else {  // if skater id does NOT match ScoreBoard.Team('team').Skater('id').Number
 		// Look for penalty
 		match = k.match(penaltyRegex);
 		if (match == null || match.length == 0)
@@ -82,8 +82,8 @@ function displayPenalty(t, s, p) { // team skater penalty#
 	var penaltyBoxd = $('.Teamd' + t + ' .Skater.Penalty[id=' + s + '] .Box' + p);
 	var totalBoxd = $('.Teamd' + t + ' .Skater.Penalty[id=' + s + '] .Total');
 
-	var prefix = 'Game.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
-	var nprefix = 'Game.Team(' + t + ').Skater(' + s + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
+	var nprefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ')';
 	code = WS.state[prefix + ".Code"];
 	if (code != null) {
 		penaltyBoxd.data("id", WS.state[prefix + ".Id"]);
@@ -123,7 +123,7 @@ function makeSkaterRows(t, id, number) { //team, id, number
         $('.Team' + t + 'custColor').css('background-color', teamBColor);
 	
 	pd.append($('<div width=10%>').addClass('Number').text(number));
-	pd.append($('<div width=50%>').addClass('Name').text(WS.state['Game.Team(' + t + ').Skater(' + id + ').Name']));
+	pd.append($('<div width=50%>').addClass('Name').text(WS.state['ScoreBoard.Team(' + t + ').Skater(' + id + ').Name']));
 	$.each([1, 2, 3, 4, 5, 6, 7, 8, 9], function(idx, c) {
 		pd.append($('<div width=10%>').addClass('Box Box' + c).html('&nbsp;'));
 	});
diff --git a/html/views/wb/tcdg2016wb.js b/html/views/wb/tcdg2016wb.js
index 759fd75e..b9e1d943 100755
--- a/html/views/wb/tcdg2016wb.js
+++ b/html/views/wb/tcdg2016wb.js
@@ -40,8 +40,8 @@ function initialize() {
         WS.Register( [ 'ScoreBoard.Team(1).Logo' ], function(k, v) { $('.Logo1').attr('src', v); } );
         WS.Register( [ 'ScoreBoard.Team(2).Logo' ], function(k, v) { $('.Logo2').attr('src', v); } );
 
-	WS.Register( [ 'Game.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
-	WS.Register( [ 'Game.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
+	WS.Register( [ 'ScoreBoard.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
+	WS.Register( [ 'ScoreBoard.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
 
 }
 
@@ -53,8 +53,8 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 	if (match == null || match.length == 0)
 		return;
 	var id = match[1]; // id = skater id
-	var prefix = 'Game.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = Game.Team('1').Skater('id')
-	if (k == prefix + '.Number') { //Example if skater id == Game.Team('team').Skater('id').Number
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = ScoreBoard.Team('1').Skater('id')
+	if (k == prefix + '.Number') { //Example if skater id == ScoreBoard.Team('team').Skater('id').Number
 		var rowd = $('.Teamd' + t + ' .Skater.Penalty[id=' + id + ']');
 		if (v == null) { // if jam number is null
 			$('.Teamd' + t + ' .Skater[id=' + id + ']').remove();
@@ -67,7 +67,7 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 		for (var i = 1; i <= 9; i++) { // for penalty numbers one to nine..
 			displayPenalty(t, id, i); } // display penalties (team, skater id, penalty #)
 		displayPenalty(t, id, 'FO_EXP'); // display foulout status
-	} else {  // if skater id does NOT match Game.Team('team').Skater('id').Number
+	} else {  // if skater id does NOT match ScoreBoard.Team('team').Skater('id').Number
 		// Look for penalty
 		match = k.match(penaltyRegex);
 		if (match == null || match.length == 0)
@@ -81,8 +81,8 @@ function displayPenalty(t, s, p) { // team skater penalty#
 	var penaltyBoxd = $('.Teamd' + t + ' .Skater.Penalty[id=' + s + '] .Box' + p);
 	var totalBoxd = $('.Teamd' + t + ' .Skater.Penalty[id=' + s + '] .Total');
 
-	var prefix = 'Game.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
-	var nprefix = 'Game.Team(' + t + ').Skater(' + s + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
+	var nprefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ')';
 	code = WS.state[prefix + ".Code"];
 	if (code != null) {
 		penaltyBoxd.data("id", WS.state[prefix + ".Id"]);
@@ -122,7 +122,7 @@ function makeSkaterRows(t, id, number) { //team, id, number
         $('.Team' + t + 'custColor').css('background-color', teamBColor);
 	
 	pd.append($('<div width=10%>').addClass('Number').text(number));
-	pd.append($('<div width=50%>').addClass('Name').text(WS.state['Game.Team(' + t + ').Skater(' + id + ').Name']));
+	pd.append($('<div width=50%>').addClass('Name').text(WS.state['ScoreBoard.Team(' + t + ').Skater(' + id + ').Name']));
 	$.each([1, 2, 3, 4, 5, 6, 7, 8, 9], function(idx, c) {
 		pd.append($('<div width=10%>').addClass('Box Box' + c).html('&nbsp;'));
 	});
diff --git a/html/views/wb/whiteboard.js b/html/views/wb/whiteboard.js
index 87040f64..8b6f7c0e 100755
--- a/html/views/wb/whiteboard.js
+++ b/html/views/wb/whiteboard.js
@@ -26,8 +26,8 @@ function initialize() {
 	WS.Register( [ 'ScoreBoard.Clock(Period).Number' ], function(k, v) { period = v; });
 	WS.Register( [ 'ScoreBoard.Clock(Jam).Number' ], function(k, v) { jam = v; });
 
-	WS.Register( [ 'Game.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
-	WS.Register( [ 'Game.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
+	WS.Register( [ 'ScoreBoard.Team(1).Skater' ], function(k, v) { skaterUpdate(1, k, v); } ); //called when skater info changes?
+	WS.Register( [ 'ScoreBoard.Team(2).Skater' ], function(k, v) { skaterUpdate(2, k, v); } ); //arguments: team number, skater id, jam number
 	WS.Register( [ 'PenaltyCode' ], function(k, v) { penaltyCode(k, v); } );
 	WS.Register( [ 'ScoreBoard.Clock(Period).MinimumNumber', 'ScoreBoard.Clock(Period).MaximumNumber' ], function(k, v) { setupSelect('Period'); } );
 	WS.Register( [ 'ScoreBoard.Clock(Jam).MinimumNumber', 'ScoreBoard.Clock(Jam).MaximumNumber' ], function(k, v) { setupSelect('Jam'); } );
@@ -57,8 +57,8 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 	if (match == null || match.length == 0)
 		return;
 	var id = match[1]; // id = skater id
-	var prefix = 'Game.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = Game.Team('1').Skater('id')
-	if (k == prefix + '.Number') { //Example if skater id == Game.Team('team').Skater('id').Number
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + id + ')';  //Example: prefix = ScoreBoard.Team('1').Skater('id')
+	if (k == prefix + '.Number') { //Example if skater id == ScoreBoard.Team('team').Skater('id').Number
 		var row = $('.Team' + t + ' .Skater.Penalty[id=' + id + ']');
 		if (v == null) { // if jam number is null
 			$('.Team' + t + ' .Skater[id=' + id + ']').remove();
@@ -71,7 +71,7 @@ function skaterUpdate(t, k, v) { //arguments: team number, skater id, jam number
 		for (var i = 1; i <= 9; i++) // for penalty numbers one to nine..
 			displayPenalty(t, id, i); // display penalties (team, skater id, penalty #)
 		displayPenalty(t, id, 'FO_EXP'); // display foulout status
-	} else {  // if skater id does NOT match Game.Team('team').Skater('id').Number
+	} else {  // if skater id does NOT match ScoreBoard.Team('team').Skater('id').Number
 		// Look for penalty
 		match = k.match(penaltyRegex);
 		if (match == null || match.length == 0)
@@ -86,7 +86,7 @@ function displayPenalty(t, s, p) { // team skater penalty#
 	var jamBox = $('.Team' + t + ' .Skater.Jam[id=' + s + '] .Box' + p);
 	var totalBox = $('.Team' + t + ' .Skater.Penalty[id=' + s + '] .Total');
 
-	var prefix = 'Game.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
+	var prefix = 'ScoreBoard.Team(' + t + ').Skater(' + s + ').Penalty(' + p + ')';
 	code = WS.state[prefix + ".Code"];
 
 	if (code != null) {
diff --git a/src/com/carolinarollergirls/scoreboard/Game.java b/src/com/carolinarollergirls/scoreboard/Game.java
index 2dfb42f8..92e3dd32 100644
--- a/src/com/carolinarollergirls/scoreboard/Game.java
+++ b/src/com/carolinarollergirls/scoreboard/Game.java
@@ -1,5 +1,6 @@
 package com.carolinarollergirls.scoreboard;
 
+import io.prometheus.client.Histogram;
 import java.io.File;
 import java.io.FileWriter;
 import java.util.ArrayList;
@@ -115,30 +116,6 @@ public class Game {
 		return sb.getClock(id);
 	}
 
-	public SkaterInfo getSkater(String teamId, String skaterId) {
-		for (TeamInfo t : teams) {
-			if (t.getTeam().equals(teamId)) {
-				for (SkaterInfo s : t.getSkaters()) {
-					if (s.getId().equals(skaterId)) {
-						return s;
-					}
-				}
-			}
-		}
-		return null;
-	}
-
-	public void Penalty(String teamId, String skaterId, String penaltyId, boolean fo_exp, int period, int jam, String code) {
-		synchronized (saveLock) {
-			SkaterInfo si = getSkater(teamId, skaterId);
-			if (si == null)
-				return;
-
-			si.Penalty(penaltyId, fo_exp, period, jam, code);
-			saveLock.notifyAll();
-		}
-	}
-
 	public String getUpdaterBase() { return "Game"; }
 
 	private void saveFile() {
@@ -168,6 +145,7 @@ public class Game {
 				File file = new File(new File(ScoreBoardManager.getDefaultPath(), "GameData"), identifier.replaceAll("[^a-zA-Z0-9\\.\\-]", "_") + ".json");
 				file.getParentFile().mkdirs();
 				FileWriter out = null;
+				Histogram.Timer timer = saveDuration.startTimer();
 				try {
 					out = new FileWriter(file);
 					out.write(toJSON().toString(2));
@@ -178,6 +156,7 @@ public class Game {
 					if (out != null) {
 						try { out.close(); } catch (Exception e) { }
 					}
+					timer.observeDuration();
 				}
 			}
 		}
@@ -205,4 +184,6 @@ public class Game {
 	private boolean logging = false;
 	private String identifier = "";
 	private Object saveLock = new Object();
+	private static final Histogram saveDuration = Histogram.build()
+	  .name("crg_game_save_duration_seconds").help("Time spent saving JSON to disk").register();
 }
diff --git a/src/com/carolinarollergirls/scoreboard/Penalty.java b/src/com/carolinarollergirls/scoreboard/Penalty.java
deleted file mode 100644
index 5832aa46..00000000
--- a/src/com/carolinarollergirls/scoreboard/Penalty.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.carolinarollergirls.scoreboard;
-/**
- * Copyright (C) 2008-2012 Mr Temper <MrTemper@CarolinaRollergirls.com>
- *
- * This file is part of the Carolina Rollergirls (CRG) ScoreBoard.
- * The CRG ScoreBoard is licensed under either the GNU General Public
- * License version 3 (or later), or the Apache License 2.0, at your option.
- * See the file COPYING for details.
- */
-
-import com.carolinarollergirls.scoreboard.event.*;
-
-public interface Penalty extends ScoreBoardEventProvider
-{
-
-}
diff --git a/src/com/carolinarollergirls/scoreboard/Skater.java b/src/com/carolinarollergirls/scoreboard/Skater.java
index a29bc142..f931f128 100644
--- a/src/com/carolinarollergirls/scoreboard/Skater.java
+++ b/src/com/carolinarollergirls/scoreboard/Skater.java
@@ -21,10 +21,27 @@ public interface Skater extends ScoreBoardEventProvider
 	public String getPosition();
 	public boolean isPenaltyBox();
 	public String getFlags();
+  public List<Penalty> getPenalties();
+  public Penalty getFOEXPPenalty();
 
 	public static final String EVENT_NAME = "Name";
 	public static final String EVENT_NUMBER = "Number";
 	public static final String EVENT_POSITION = "Position";
 	public static final String EVENT_PENALTY_BOX = "PenaltyBox";
 	public static final String EVENT_FLAGS = "Flags";
+
+  public static final String EVENT_PENALTY = "Penalty";
+  public static final String EVENT_REMOVE_PENALTY = "RemovePenalty";
+  public static final String EVENT_PENALTY_FOEXP = "PenaltyFOEXP";
+  public static final String EVENT_PENALTY_REMOVE_FOEXP = "RemovePenaltyFOEXP";
+  public static final String EVENT_PENALTY_PERIOD = "Period";
+  public static final String EVENT_PENALTY_JAM = "Jam";
+  public static final String EVENT_PENALTY_CODE = "Code";
+
+	public static interface Penalty extends ScoreBoardEventProvider {
+		public String getId();
+		public int getPeriod();
+		public int getJam();
+		public String getCode();
+	}
 }
diff --git a/src/com/carolinarollergirls/scoreboard/defaults/DefaultScoreBoardModel.java b/src/com/carolinarollergirls/scoreboard/defaults/DefaultScoreBoardModel.java
index bdbfc490..1768432d 100644
--- a/src/com/carolinarollergirls/scoreboard/defaults/DefaultScoreBoardModel.java
+++ b/src/com/carolinarollergirls/scoreboard/defaults/DefaultScoreBoardModel.java
@@ -430,6 +430,10 @@ public class DefaultScoreBoardModel extends DefaultScoreBoardEventProvider imple
 		}
 	}
 
+  public void penalty(String teamId, String skaterId, String penaltyId, boolean fo_exp, int period, int jam, String code){
+    getTeamModel(teamId).penalty(skaterId, penaltyId, fo_exp, period, jam, code);
+  }
+
 	public Settings getSettings() { return (Settings)settings; }
 	public SettingsModel getSettingsModel() { return settings; }
 
diff --git a/src/com/carolinarollergirls/scoreboard/defaults/DefaultSkaterModel.java b/src/com/carolinarollergirls/scoreboard/defaults/DefaultSkaterModel.java
index 8bc9ff2d..9bc85eb6 100644
--- a/src/com/carolinarollergirls/scoreboard/defaults/DefaultSkaterModel.java
+++ b/src/com/carolinarollergirls/scoreboard/defaults/DefaultSkaterModel.java
@@ -117,6 +117,50 @@ public class DefaultSkaterModel extends DefaultScoreBoardEventProvider implement
 		}
 	}
 
+  public List<Penalty> getPenalties() { return Collections.unmodifiableList(new ArrayList<Penalty>(penalties)); }
+  public Penalty getFOEXPPenalty() { return foexp_penalty; }
+  public void AddPenaltyModel(String id, boolean foulout_explusion, int period, int jam, String code) {
+    synchronized (penaltiesLock) {
+      if (foulout_explusion && code != null) {
+          Penalty prev = foexp_penalty;
+          id = UUID.randomUUID().toString();
+          if (prev != null) {
+            id = prev.getId();
+          }
+          foexp_penalty = new DefaultPenaltyModel(id, period, jam, code);
+          scoreBoardChange(new ScoreBoardEvent(getSkater(), EVENT_PENALTY_FOEXP, foexp_penalty, null));
+      } else if (foulout_explusion && code == null) {
+        Penalty prev = foexp_penalty;
+        foexp_penalty = null;
+        scoreBoardChange(new ScoreBoardEvent(getSkater(), EVENT_PENALTY_REMOVE_FOEXP, null, prev));
+      } else if (id == null ) {
+        id = UUID.randomUUID().toString();
+        // Non FO/Exp, make sure skater has 9 or less regular penalties before adding another
+        if (penalties.size() < 9) {
+          DefaultPenaltyModel dpm = new DefaultPenaltyModel(id, period, jam, code);
+          penalties.add(dpm);
+          scoreBoardChange(new ScoreBoardEvent(getSkater(), EVENT_PENALTY, dpm, null));
+        }
+      } else {
+        // Updating/Deleting existing Penalty.  Find it and process
+        for (DefaultPenaltyModel p2 : penalties) {
+          if (p2.getId().equals(id)) {
+            if (code != null) {
+              p2.period = period;
+              p2.jam = jam;
+              p2.code = code;
+              scoreBoardChange(new ScoreBoardEvent(getSkater(), EVENT_PENALTY, p2, null));
+            } else {
+              penalties.remove(p2);
+              scoreBoardChange(new ScoreBoardEvent(getSkater(), EVENT_REMOVE_PENALTY, null, p2));
+            }
+            break;
+          }
+        }
+      }
+    }
+  }
+
 	public void bench() {
 		synchronized (positionLock) {
 			saved_position = position;
@@ -141,6 +185,8 @@ public class DefaultSkaterModel extends DefaultScoreBoardEventProvider implement
 	protected String position = Position.ID_BENCH;
 	protected boolean penaltyBox = false;
 	protected String flags;
+	protected List<DefaultPenaltyModel> penalties = new LinkedList<DefaultPenaltyModel>();
+	protected PenaltyModel foexp_penalty;
 
 	private String saved_position = Position.ID_BENCH;
 
@@ -148,6 +194,31 @@ public class DefaultSkaterModel extends DefaultScoreBoardEventProvider implement
 	protected Object numberLock = new Object();
 	protected Object positionLock = new Object();
 	protected Object flagsLock = new Object();
+	protected Object penaltiesLock = new Object();
 
 	protected boolean settingPositionSkater = false;
+
+  public class DefaultPenaltyModel extends DefaultScoreBoardEventProvider implements PenaltyModel
+  { 
+    public DefaultPenaltyModel(String i, int p, int j, String c) {
+      id = i;
+      period = p;
+      jam = j;
+      code = c;
+    }
+    public String getId() { return id; }
+    public int getPeriod() { return period; }
+    public int getJam() { return jam; }
+    public String getCode() { return code; }
+
+    public String getProviderName() { return "Penalty"; }
+    public Class getProviderClass() { return Penalty.class; }
+    public String getProviderId() { return getId(); }
+
+    protected String id;
+    protected int period;
+    protected int jam;
+    protected String code;
+  }
+
 }
diff --git a/src/com/carolinarollergirls/scoreboard/defaults/DefaultTeamModel.java b/src/com/carolinarollergirls/scoreboard/defaults/DefaultTeamModel.java
index c3e5cb33..823823bf 100644
--- a/src/com/carolinarollergirls/scoreboard/defaults/DefaultTeamModel.java
+++ b/src/com/carolinarollergirls/scoreboard/defaults/DefaultTeamModel.java
@@ -476,6 +476,11 @@ public class DefaultTeamModel extends DefaultScoreBoardEventProvider implements
 		}
 	}
 
+  public void penalty(String skaterId, String penaltyId, boolean fo_exp, int period, int jam, String code) {
+    getSkaterModel(skaterId).AddPenaltyModel(penaltyId, fo_exp, period, jam, code);
+  }
+
+
 
 	protected ScoreBoardModel scoreBoardModel;
 
diff --git a/src/com/carolinarollergirls/scoreboard/game/SkaterInfo.java b/src/com/carolinarollergirls/scoreboard/game/SkaterInfo.java
index 3c3e073e..caceb3a8 100644
--- a/src/com/carolinarollergirls/scoreboard/game/SkaterInfo.java
+++ b/src/com/carolinarollergirls/scoreboard/game/SkaterInfo.java
@@ -28,8 +28,16 @@ public class SkaterInfo extends Updater {
 		number = s.getNumber();
 		flags = s.getFlags();
 
-		queueUpdates();
-	}
+    penalties.clear();
+    for (Skater.Penalty p : s.getPenalties()) {
+      penalties.add(new Penalty(game, p.getId(), p.getPeriod(), p.getJam(), p.getCode()));
+    }
+    fo_exp = null;
+    Skater.Penalty fe = s.getFOEXPPenalty();
+    if (fe != null) {
+      fo_exp = new Penalty(game, fe.getId(), fe.getPeriod(), fe.getJam(), fe.getCode());
+    }
+  }
 
 	public JSONObject toJSON() throws JSONException {
 		JSONObject json = new JSONObject();
@@ -50,59 +58,6 @@ public class SkaterInfo extends Updater {
 	}
 
 	private void queueUpdates() {
-		update("Id", id);
-		update("Name", name);
-		update("Number", number);
-		update("Flags", flags);
-
-		for (int i = 0; i < 9; i++) {
-			String base = "Penalty(" + (i + 1) + ")";
-			if (i < penalties.size())
-				penalties.get(i).queueUpdates(getUpdaterBase() + "." + base);
-			else
-				update(base, null);
-		}
-		if (fo_exp != null)
-			fo_exp.queueUpdates(getUpdaterBase() + ".Penalty(FO_EXP)");
-		else
-			update("Penalty(FO_EXP)", null);
-	}
-
-	public void Penalty(String penaltyId, boolean fe, int p, int j, String c) {
-		if (penaltyId == null || (fe && c != null)) {
-			// New Penalty (Or FO/Exp, just overwrite it)
-			newPenalty(fe, p, j, c);
-		} else if (fe) {
-			// Must be a delete request
-			fo_exp = null;
-		} else {
-			// Updating/Deleting existing Penalty.  Find it and process
-			for (Penalty p2 : penalties) {
-				if (p2.getId().equals(penaltyId)) {
-					if (c != null) {
-						p2.period = p;
-						p2.jam = j;
-						p2.code = c;
-					} else {
-						penalties.remove(p2);
-					}
-					break;
-				}
-			}
-		}
-		queueUpdates();
-		updateState();
-	}
-
-	private void newPenalty(boolean fe, int p, int j, String c) {
-		if (fe) {
-			fo_exp = new Penalty(game, p, j, c);
-		} else {
-			// Non FO/Exp, make sure skater has 9 or less regular penalties before adding another
-			if (penalties.size() < 9) {
-				penalties.add(new Penalty(game, p, j, c));
-			}
-		}
 	}
 
 	public TeamInfo      getTeamInfo()  { return team_info; }
@@ -122,20 +77,15 @@ public class SkaterInfo extends Updater {
 	private Penalty fo_exp;
 
 	public class Penalty extends Updater {
-		public Penalty(Game g, int p, int j, String c) {
+		public Penalty(Game g, String i, int p, int j, String c) {
 			super(g);
-			id = UUID.randomUUID().toString();
+			id = i;
 			period = p;
 			jam = j;
 			code = c;
 		}
 
 		public void queueUpdates(String base) {
-			this.base = base;
-			update("Id", id);
-			update("Period", period);
-			update("Jam", jam);
-			update("Code", code);
 		}
 
 		public String getUpdaterBase() {
diff --git a/src/com/carolinarollergirls/scoreboard/jetty/WS.java b/src/com/carolinarollergirls/scoreboard/jetty/WS.java
index 04c0cf12..83f15884 100644
--- a/src/com/carolinarollergirls/scoreboard/jetty/WS.java
+++ b/src/com/carolinarollergirls/scoreboard/jetty/WS.java
@@ -10,6 +10,7 @@ package com.carolinarollergirls.scoreboard.jetty;
 
 import io.prometheus.client.Counter;
 import io.prometheus.client.Gauge;
+import io.prometheus.client.Histogram;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -30,10 +31,12 @@ import org.json.JSONObject;
 import com.carolinarollergirls.scoreboard.ScoreBoard;
 import com.carolinarollergirls.scoreboard.ScoreBoardManager;
 import com.carolinarollergirls.scoreboard.json.ScoreBoardJSONListener;
+import com.carolinarollergirls.scoreboard.model.ScoreBoardModel;
 
 public class WS extends WebSocketServlet {
-	public WS(ScoreBoard sb) {
-		ScoreBoardJSONListener listener = new ScoreBoardJSONListener(sb);
+	public WS(ScoreBoardModel s) {
+    sbm = s;
+		ScoreBoardJSONListener listener = new ScoreBoardJSONListener(s);
 	}
 
 	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
@@ -58,16 +61,6 @@ public class WS extends WebSocketServlet {
 		}
 	}
 
-	public static void requestUpdates(UUID id, String path) {
-		synchronized (sourcesLock) {
-			for (Conn source : sources) {
-				if (source.id.equals(id)) {
-					source.requestUpdates(path);
-				}
-			}
-		}
-	}
-
 	private static void updateState(String key, Object value) {
 		Map<String, Object> updateMap = new LinkedHashMap<String, Object>();
 		updateMap.put(key, value);
@@ -75,6 +68,7 @@ public class WS extends WebSocketServlet {
 	}
 
 	public static void updateState(Map<String, Object> updates) {
+		Histogram.Timer timer = updateStateDuration.startTimer();
 		synchronized (sourcesLock) {
 			stateID++;
 			List<String> keys = new LinkedList<String>(updates.keySet());
@@ -106,11 +100,14 @@ public class WS extends WebSocketServlet {
 			keys.addAll(deletedKeys);
 
 			for (Conn source : sources) {
-				source.sendUpdates(keys);
+				source.sendUpdates();
 			}
 		}
+		timer.observeDuration();
+		updateStateUpdates.observe(updates.size());
 	}
 
+	protected static ScoreBoardModel sbm;
 	protected static List<Conn> sources = new LinkedList<Conn>();
 	protected static Object sourcesLock = new Object();
 
@@ -131,14 +128,19 @@ public class WS extends WebSocketServlet {
 		return true;
 	}
 
+	private static final Histogram updateStateDuration = Histogram.build()
+		.name("crg_websocket_update_state_duration_seconds").help("Time spent in WS.updateState function").register();
+	private static final Histogram updateStateUpdates = Histogram.build()
+		.name("crg_websocket_update_state_updates").help("Updates sent to WS.updateState function")
+		.exponentialBuckets(1, 2, 10).register();
 	private static final Gauge connectionsActive = Gauge.build()
-		.name("websocket_active_connections").help("Current WebSocket connections").register();
+		.name("crg_websocket_active_connections").help("Current WebSocket connections").register();
 	private static final Counter messagesReceived = Counter.build()
-		.name("websocket_messages_received").help("Number of WebSocket messages received").register();
-	private static final Counter messagesSent = Counter.build()
-		.name("websocket_messages_sent").help("Number of WebSocket messages sent").register();
+		.name("crg_websocket_messages_received").help("Number of WebSocket messages received").register();
+	private static final Histogram messagesSentDuration = Histogram.build()
+		.name("crg_websocket_messages_sent_duration_seconds").help("Time spent sending WebSocket messages").register();
 	private static final Counter messagesSentFailures = Counter.build()
-		.name("websocket_messages_sent_failed").help("Number of WebSocket messages we failed to send").register();
+		.name("crg_websocket_messages_sent_failed").help("Number of WebSocket messages we failed to send").register();
 
 	public class Conn implements OnTextMessage {
 		private Connection connection;
@@ -163,7 +165,7 @@ public class WS extends WebSocketServlet {
 						for (int i = 0; i < paths.length(); i++)
 							requestUpdates(paths.getString(i));
 					}
-					sendUpdates();
+					sendPendingUpdates();
 				} else if (action.equals("Penalty")) {
 					JSONObject data = json.getJSONObject("data");
 					String teamId = data.optString("teamId");
@@ -175,7 +177,7 @@ public class WS extends WebSocketServlet {
 					String code = data.optString("code", null);
 					if (period == -1 || jam == -1)
 						return;
-					ScoreBoardManager.getGame().Penalty(teamId, skaterId, penaltyId, fo_exp, period, jam, code);
+					sbm.penalty(teamId, skaterId, penaltyId, fo_exp, period, jam, code);
 				} else if (action.equals("Set")) {
 					String key = json.getString("key");
 					Object value = json.get("value");
@@ -197,7 +199,7 @@ public class WS extends WebSocketServlet {
 		}
 		
 		public void send(JSONObject json) {
-			messagesSent.inc();
+			Histogram.Timer timer = messagesSentDuration.startTimer();
 			try {
 				json.put("stateID", stateID);
 				connection.sendMessage(json.toString());
@@ -205,6 +207,8 @@ public class WS extends WebSocketServlet {
 				ScoreBoardManager.printMessage("Error sending JSON update: " + e);
 				e.printStackTrace();
 				messagesSentFailures.inc();
+			} finally {
+				timer.observeDuration();
 			}
 		}
 	
@@ -297,7 +301,7 @@ public class WS extends WebSocketServlet {
 			}
 		}
 
-		private void sendUpdates() {
+		private void sendPendingUpdates() {
 			synchronized (this) {
 				if (updates.size() == 0)
 					return;
@@ -322,12 +326,12 @@ public class WS extends WebSocketServlet {
 			}
 		}
 
-		public void sendUpdates(List<String> paths) {
+		public void sendUpdates() {
 			synchronized (this) {
 				for (String p : paths) {
 					processUpdates(p, false);
 				}
-				sendUpdates();
+				sendPendingUpdates();
 			}
 		}
 
diff --git a/src/com/carolinarollergirls/scoreboard/json/ScoreBoardJSONListener.java b/src/com/carolinarollergirls/scoreboard/json/ScoreBoardJSONListener.java
index be42ac5c..2e02b4b4 100644
--- a/src/com/carolinarollergirls/scoreboard/json/ScoreBoardJSONListener.java
+++ b/src/com/carolinarollergirls/scoreboard/json/ScoreBoardJSONListener.java
@@ -10,6 +10,7 @@ package com.carolinarollergirls.scoreboard.json;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.List;
 
 import com.carolinarollergirls.scoreboard.*;
 import com.carolinarollergirls.scoreboard.model.SettingsModel;
@@ -58,6 +59,14 @@ public class ScoreBoardJSONListener implements ScoreBoardListener
 						processAlternateName(childPath, (Team.AlternateName)v, prop.equals(Team.EVENT_REMOVE_ALTERNATE_NAME));
 					else if (v instanceof Team.Color)
 						processColor(childPath, (Team.Color)v, prop.equals(Team.EVENT_REMOVE_COLOR));
+          // Fast path for jam start/end to avoid sending the entire team.
+          else if (prop.equals(Team.EVENT_LAST_SCORE)) {
+            updateMap.put(childPath + ".Team(" + t.getId() + ")." + Team.EVENT_LAST_SCORE, t.getLastScore());
+            updateMap.put(childPath + ".Team(" + t.getId() + ").JamScore", t.getScore() - t.getLastScore());
+          } else if (prop.equals(Team.EVENT_LEAD_JAMMER))
+            updateMap.put(childPath + ".Team(" + t.getId() + ")." + Team.EVENT_LEAD_JAMMER, t.getLeadJammer());
+          else if (prop.equals(Team.EVENT_STAR_PASS))
+            updateMap.put(childPath + ".Team(" + t.getId() + ")." + Team.EVENT_STAR_PASS, t.isStarPass());
 					else
 						processTeam("ScoreBoard", t, prop.equals(ScoreBoard.EVENT_REMOVE_TEAM));
 				} else if (p instanceof Skater) {
@@ -154,8 +163,29 @@ public class ScoreBoardJSONListener implements ScoreBoardListener
 		updateMap.put(path + "." + Skater.EVENT_POSITION, s.getPosition());
 		updateMap.put(path + "." + Skater.EVENT_FLAGS, s.getFlags());
 		updateMap.put(path + "." + Skater.EVENT_PENALTY_BOX, s.isPenaltyBox());
+
+    List<Skater.Penalty> penalties = s.getPenalties();
+    for (int i = 0; i < 9; i++) {
+      String base = path + ".Penalty(" + (i + 1) + ")";
+      if (i < penalties.size())
+        processPenalty(base, penalties.get(i), false);
+      else
+        processPenalty(base, null, true);
+    }
+		processPenalty(path + ".Penalty(FO_EXP)", s.getFOEXPPenalty(), s.getFOEXPPenalty() == null);
 	}
 
+	private void processPenalty(String path, Skater.Penalty p, boolean remove) {
+		if (remove) {
+			updateMap.put(path, null);
+			return;
+		}
+    updateMap.put(path + ".Id", p.getId());
+    updateMap.put(path + ".Period", p.getPeriod());
+    updateMap.put(path + ".Jam", p.getJam());
+    updateMap.put(path + ".Code", p.getCode());
+  }
+
 	private void processTeam(String path, Team t, boolean remove) {
 		path = path + ".Team(" + t.getId() + ")";
 		if (remove) {
diff --git a/src/com/carolinarollergirls/scoreboard/model/ScoreBoardModel.java b/src/com/carolinarollergirls/scoreboard/model/ScoreBoardModel.java
index dbbc44d8..4aebad63 100644
--- a/src/com/carolinarollergirls/scoreboard/model/ScoreBoardModel.java
+++ b/src/com/carolinarollergirls/scoreboard/model/ScoreBoardModel.java
@@ -40,6 +40,8 @@ public interface ScoreBoardModel extends ScoreBoard
 	public void unStopJam();
 	public void unTimeout();
 
+  public void penalty(String teamId, String skaterId, String penaltyId, boolean fo_exp, int period, int jam, String code);
+
 	public void setRuleset(String id);
 	public SettingsModel getSettingsModel();
 
diff --git a/src/com/carolinarollergirls/scoreboard/model/SkaterModel.java b/src/com/carolinarollergirls/scoreboard/model/SkaterModel.java
index 77cd0e34..c8eeadd8 100644
--- a/src/com/carolinarollergirls/scoreboard/model/SkaterModel.java
+++ b/src/com/carolinarollergirls/scoreboard/model/SkaterModel.java
@@ -23,4 +23,10 @@ public interface SkaterModel extends Skater
 	public void setPosition(String position) throws PositionNotFoundException;
 	public void setPenaltyBox(boolean box);
 	public void setFlags(String flags);
+
+  // A null code removes the penalty.  
+  public void AddPenaltyModel(String id, boolean foulout_explusion, int period, int jam, String code);
+
+	public static interface PenaltyModel extends Penalty {
+	}
 }
diff --git a/src/com/carolinarollergirls/scoreboard/model/TeamModel.java b/src/com/carolinarollergirls/scoreboard/model/TeamModel.java
index deb13c3f..ce53a57f 100644
--- a/src/com/carolinarollergirls/scoreboard/model/TeamModel.java
+++ b/src/com/carolinarollergirls/scoreboard/model/TeamModel.java
@@ -75,6 +75,8 @@ public interface TeamModel extends Team
 
 	public void setStarPass(boolean starPass);
 
+  public void penalty(String skaterId, String penaltyId, boolean fo_exp, int period, int jam, String code);
+
 	public static interface AlternateNameModel extends AlternateName {
 		public void setName(String n);
 
diff --git a/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlConverter.java b/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlConverter.java
index 4ff90671..86d6439d 100644
--- a/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlConverter.java
+++ b/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlConverter.java
@@ -194,9 +194,26 @@ public class ScoreBoardXmlConverter
 		editor.setElement(e, Skater.EVENT_PENALTY_BOX, null, String.valueOf(s.isPenaltyBox()));
 		editor.setElement(e, Skater.EVENT_FLAGS, null, s.getFlags());
 
+    for (Skater.Penalty p: s.getPenalties()) {
+			toElement(e, p);
+    }
+
+    if (s.getFOEXPPenalty() != null) {
+      Element fe = editor.setElement(e, Skater.EVENT_PENALTY_FOEXP, s.getFOEXPPenalty().getId());
+      toElement(fe, s.getFOEXPPenalty());
+    }
+
 		return e;
 	}
 
+	public Element toElement(Element s, Skater.Penalty p) {
+		Element e = editor.setElement(s, "Penalty", p.getId());
+		editor.setElement(e, Skater.EVENT_PENALTY_PERIOD, null, String.valueOf(p.getPeriod()));
+		editor.setElement(e, Skater.EVENT_PENALTY_JAM, null, String.valueOf(p.getJam()));
+		editor.setElement(e, Skater.EVENT_PENALTY_CODE, null, p.getCode());
+    return e;
+  }
+
 	/*****************************/
 	/* XML to ScoreBoard methods */
 
@@ -570,7 +587,11 @@ public class ScoreBoardXmlConverter
 
 				boolean isChange = Boolean.parseBoolean(element.getAttributeValue("change"));
 
-				if (null == value)
+				if (name.equals(Skater.EVENT_PENALTY))
+					processPenalty(skaterModel, element, false);
+				else if (name.equals(Skater.EVENT_PENALTY_FOEXP))
+					processPenalty(skaterModel, element.getChild(Skater.EVENT_PENALTY), true);
+        else if (null == value)
 					continue;
 				else if (name.equals(Skater.EVENT_NAME))
 					skaterModel.setName(value);
@@ -587,6 +608,33 @@ public class ScoreBoardXmlConverter
 		}
 	}
 
+	public void processPenalty(SkaterModel skaterModel, Element penalty, boolean foulout_exp) {
+		String id = penalty.getAttributeValue("Id");
+    int period = 0;
+    int jam = 0;
+    String code = "";
+
+		Iterator children = penalty.getChildren().iterator();
+		while (children.hasNext()) {
+			Element element = (Element)children.next();
+      try {
+				String name = element.getName();
+				String value = editor.getText(element);
+
+				if (null == value)
+					continue;
+				else if (name.equals(Skater.EVENT_PENALTY_PERIOD))
+					period = Integer.parseInt(value);
+				else if (name.equals(Skater.EVENT_PENALTY_JAM))
+					jam = Integer.parseInt(value);
+				else if (name.equals(Skater.EVENT_PENALTY_CODE))
+					code = value;
+			} catch ( Exception e ) {
+			}
+    }
+    skaterModel.AddPenaltyModel(null, foulout_exp, period, jam, code);
+  }
+
 	public static ScoreBoardXmlConverter getInstance() { return scoreBoardXmlConverter; }
 
 	protected XmlDocumentEditor editor = new XmlDocumentEditor();
diff --git a/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlListener.java b/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlListener.java
index ab080d9c..5a2088c1 100644
--- a/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlListener.java
+++ b/src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlListener.java
@@ -152,7 +152,30 @@ public class ScoreBoardXmlListener implements ScoreBoardListener
 		} else if (p.getProviderName().equals("Color")) {
 			editor.setElement(getColorElement((Team.Color)p), prop, null, v);
 		} else if (p.getProviderName().equals("Skater")) {
-			editor.setElement(getSkaterElement((Skater)p), prop, null, v);
+      if (prop.equals(Skater.EVENT_PENALTY) || prop.equals(Skater.EVENT_PENALTY_FOEXP)) {
+        // Replace whole skater.
+				converter.toElement(getTeamElement(((Skater)p).getTeam()), (Skater)p);
+      } else if (prop.equals(Skater.EVENT_REMOVE_PENALTY)) {
+        Skater.Penalty prev = (Skater.Penalty)(event.getPreviousValue());
+        if (prev != null) {
+          if (isPersistent()) {
+            editor.removeElement(getSkaterElement((Skater)p), Skater.EVENT_PENALTY, prev.getId());
+          } else {
+            editor.setRemovePI(editor.addElement(getSkaterElement((Skater)p), Skater.EVENT_PENALTY, prev.getId()));
+          }
+        }
+      } else if (prop.equals(Skater.EVENT_PENALTY_REMOVE_FOEXP)) {
+				if (isPersistent()) {
+					editor.removeElement(getSkaterElement((Skater)p), Skater.EVENT_PENALTY_FOEXP);
+        } else {
+          Skater.Penalty prev = (Skater.Penalty)(event.getPreviousValue());
+          if (prev != null) {
+            editor.setRemovePI(editor.addElement(getSkaterElement((Skater)p), Skater.EVENT_PENALTY_FOEXP, prev.getId()));
+          }
+        }
+      } else {
+        editor.setElement(getSkaterElement((Skater)p), prop, null, v);
+      }
 		} else if (p.getProviderName().equals("Clock")) {
 			Element e = editor.setElement(getClockElement((Clock)p), prop, null, v);
 			if (prop.equals("Time")) {
['html/views/wb/whiteboard.js', 'html/views/wb/tcdg2016wb.js', 'src/com/carolinarollergirls/scoreboard/game/SkaterInfo.java', 'src/com/carolinarollergirls/scoreboard/jetty/WS.java', 'src/com/carolinarollergirls/scoreboard/json/ScoreBoardJSONListener.java', 'src/com/carolinarollergirls/scoreboard/defaults/DefaultSkaterModel.java', 'html/controls/pt/index.js', 'html/views/overlays/wb/tcdg2016wb.js', 'html/views/overlays/ddtv/admin.js', 'html/views/overlays/ddtv/index.js', 'src/com/carolinarollergirls/scoreboard/defaults/DefaultTeamModel.java', 'src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlConverter.java', 'html/views/overlays/wb/tcdg2016wb_underlay.js', 'src/com/carolinarollergirls/scoreboard/xml/ScoreBoardXmlListener.java', 'html/views/overlays/interactive/index.js', 'src/com/carolinarollergirls/scoreboard/defaults/DefaultScoreBoardModel.java', 'html/controls/pt/ptcolor.js', 'src/com/carolinarollergirls/scoreboard/model/TeamModel.java', 'src/com/carolinarollergirls/scoreboard/Game.java', 'src/com/carolinarollergirls/scoreboard/model/ScoreBoardModel.java', 'src/com/carolinarollergirls/scoreboard/Skater.java', 'src/com/carolinarollergirls/scoreboard/model/SkaterModel.java']