ID:2080606
 
BYOND Version:510.1341
Operating System:Windows 10 Pro 64-bit
Web Browser:Chrome 50.0.2661.94
Applies to:Webclient
Status: Open

Issue hasn't been assigned a status value.
Descriptive Problem Summary:

At completely random moments, the webclient is prone to stuttering / freezing for a second or two with no real indication as to what is causing it. These performance drops happen frequently enough to make consistent gameplay unbearable.

Right now, we don't have a whole lot of data on the issue - but we're working on it.
A reboot seems to improve the FPS though. I could show this issue to Lummox in-game if that'd help at all.
In-game wouldn't really help me, but any data you can get will be a big help. If you can capture the event in a profile at all, even if it's part of a larger profile and you can just narrow down for me what time frame to look at, that would be good.

Is this brand new to 1341?
This live profiler is very useful, but I don't think I'm able to save the results. This image shows the spikes pretty clearly though: http://puu.sh/oJ9ZY/edd8d3f234.png

The yellow block in 10000ms, from top to bottom:
303.79ms (self 0.08 ms) Event (load)

303.71ms (self 0.10ms) Function Call (webclient.dart.js:22733)

303.25ms (self 0.07ms) Function Call (webclient.dart.js:22733)

The yellow block just past 15000ms, from top to bottom:
236.09 ms (self 0.11 ms) Event (load)

235.99 ms (self 0.08) Function Call (webclient.dart.js:22733)

235.82 ms (self 0.p05 ms) Function Call (webclient.dart.js:22733)
You're looking at the wrong level of detail there. The function call at 22733 isn't the problem; it's something beneath it, probably far beneath it. The call you've identified is clearly a high-level one, especially with the low self-time you're seeing.
These red colored 'long frame' warnings are a bit worrying: http://puu.sh/oJftu/96051ee5ae.png

Some heavy functions:

http://puu.sh/oJj0e/69eb28a751.png
http://puu.sh/oJj2t/0f3a8bcd04.png
http://puu.sh/oJj3x/55b9419685.png
http://puu.sh/oJj4d/a06f3d0d71.png

Jank: https://developers.google.com/web/fundamentals/performance/ rendering/ "Each of those frames has a budget of just over 16ms (1 second / 60 = 16.66ms). In reality, however, the browser has housekeeping work to do, so all of your work needs to be completed inside 10ms. When you fail to meet this budget the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user's experience."

Though they mostly point to the same few dozen functions, so hopefully this isn't simply an issue of the dart code being slow overall and something more specific.

I should also mention that I'm seeing the "Optimized too many times" warning for these dart functions.


Other instances where functions exceed 10ms and cause jank:

http://puu.sh/oJgF0/0188cf9c0c.png
http://puu.sh/oJgH2/046eafe522.png
http://puu.sh/oJgIq/97765bd707.png
http://puu.sh/oJgJm/fdbd30b716.png
http://puu.sh/oJgJR/d15214227c.png
http://puu.sh/oJgKv/8636bd27a0.png


Although I don't think there's an instance in the profile where it's below 10ms, but the largest contributor by far is the dart code.
    IconInfo_closure: {
"^": "Closure:80;$this,n",
call$1: [function(bitmapData) {
var sw, tw, data, t1, t2, i, ix, iy, alphaidx, dataidx, icon, t3, ys, ny, alphamap, t4, ix0, count, iy0, ny0, bit, nbit, ix1, alphaidx0;
if (bitmapData != null) {
sw = bitmapData.width;
tw = C.JSInt_methods.$tdiv(sw, this.$this._width);
data = J.get$data$x(bitmapData.renderTextureQuad.getImageData$0(0));
} else {
sw = null;
tw = null;
data = null;
}
for (t1 = this.n, t2 = this.$this, i = 0, ix = null, iy = null, alphaidx = null, dataidx = null; i < t1; ++i) {
icon = t2.icons[i];
if (icon == null)
continue;
icon.set$bitmapDataSheet(bitmapData);
if (bitmapData != null) {
for (t3 = t2._width, ys = 1; ny = C.JSInt_methods._shlPositive$1(1, ys), ny < t3; ++ys)
;
alphamap = H.setRuntimeTypeInfo(new Array(C.JSInt_methods._shlPositive$1(t2._height, ys)), [P.$int]);
t3 = icon._frame;
t4 = C.JSInt_methods.$mod(t3, tw);
ix0 = t2._width;
t3 = C.JSInt_methods.$tdiv(t3, tw);
iy = t2._height;
dataidx = (t4 * ix0 + t3 * iy * sw) * 4;
if ($.$get$isLittleEndianSystem())
dataidx += 3;
for (alphaidx = 0, count = 0; iy0 = iy - 1, iy > 0; iy = iy0, ix = ix1) {
for (ny0 = ny, ix = ix0, bit = 0, nbit = 0; ix1 = ix - 1, ix > 0; ix = ix1) {
if (data[dataidx] !== 0) {
bit = (bit | C.JSInt_methods._shlPositive$1(1, nbit)) >>> 0;
++count;
}
++nbit;
if (nbit >= 32) {
alphaidx0 = alphaidx + 1;
alphamap[alphaidx] = bit;
--ny0;
alphaidx = alphaidx0;
bit = 0;
nbit = 0;
}
dataidx += 4;
}
for (; ny0 > 0; alphaidx = alphaidx0, bit = 0) {
alphaidx0 = alphaidx + 1;
alphamap[alphaidx] = bit;
--ny0;
}
dataidx += (sw - ix0) * 4;
}
t3 = count === 0;
icon.isEmpty = t3;
t4 = count === t2._width * t2._height;
icon.isSolid = t4;
if (!t3 && !t4) {
icon.alphaMap = alphamap;
icon.alphaYshift = ys;
}
iy = iy0;
}
icon.sync$0();
t3 = $.me.stage;
if (t3 != null) {
t3.updateAtomIcon$1(icon);
t3 = $.me;
t4 = t3.clientInfo.curCursor;
if (t4 != null && t4.icon === icon)
t3.setCursor$1(t4);
}
}
}, null, null, 2, 0, null, 22, "call"]
}


This is one of the culprits.

It looks like a big chunk of it is that it's looking for transparent pixels in the image by looping over every pixel.

It looks like it does more than that, but that's all I can tell at a glance.

In one instance, I saw this function take over 200ms.

I'm not entirely sure what's to blame, but it looks like your biggest dropoffs in framerate are coming from dealing with image data like GetImageData(). Image processing can be intense.

Also, I saw quite a few winset calls reporting 15-20+ ms just to decode, which is going to cause some jank.

You need to find a better way to cull blank/transparent icons. Looping over every pixel on load isn't viable.


You should update the DMI format to include that information in ztxt if you can. just one new field should do it: transparency = 0|1|2. 0 = no transparent pixels. 1 = at least one. 2 = no non-transparent pixels.

I know the DMM editor uses the at least one transparent pixel logic to work out what turfs can overlay one another.

Far better if you ask me to leave this part up to the C code that can pre-process this information. It should never change for the runtime, so the data can be cached. For raw PNGs, that's a bit more complicated obviously.
Having the server preprocess this would be ugly in a lot of ways. This transparency loading, though, shouldn't happen in-game unless dynamic icons get used. The point of that preloading was to avoid expensive calls on mouseover.
The point of that preloading was to avoid expensive calls on mouseover.

Then it needs to happen asynchronously. Because holding up a thread's rendering waiting for transparency loading is a bad end-user experience if it can cause 20+ dropped frames.
What I don't get is why it's happening out of nowhere, unless dynamic icons are in play.
What I don't get is why it's happening out of nowhere, unless dynamic icons are in play.

I can't say without looking at their codebase.
We are using dynamic icons, yes. Do you need specific examples?
Specific examples would probably help, especially if there's any info to be learned about the size or complexity of the icon.
The big one (and probably the only one) we use is a method that lets us generate an icon that flattens a player's overlays/underlays for things like character previews and whatnot:

proc
getFlatIcon(atom/A, dir, cache=1, list/crops = null, frame = 1, moving = 0, scale = 1) // 1 = use cache, 2 = override cache, 0 = ignore cache

if(istype(A, /mob/player))
var/mob/player/P = A
if(P.gender == "neuter")
return list(icon('empty_slot.png'), "blank")

var/list/layers = list() // Associative list of [overlay = layer]
var/hash = "" // Hash of overlay combination

if(dir == null) dir = A.dir // dir defaults to A's dir

var/parentColor = ""
if(A.color || A.alpha != 255)
parentColor = (A.color || "#FFFFFF") + copytext(rgb(0,0,0,A.alpha), 8)

// Add the atom's icon itself
if(A.icon)
// Make a copy without pixel_x/y settings
var/image/copy = image(icon=A.icon,icon_state=A.icon_state,layer=A.layer,dir=dir)
layers[copy] = A.layer

// Loop through the underlays, then overlays, sorting them into the layers list
var
list/process = A.underlays // Current list being processed
processSubset=0 // Which list is being processed: 0 = underlays, 1 = overlays

currentIndex=1 // index of 'current' in list being processed
currentOverlay // Current overlay being sorted
currentLayer // Calculated layer that overlay appears on (special case for FLOAT_LAYER)

compareOverlay // The overlay that the current overlay is being compared against
compareIndex // The index in the layers list of 'compare'
while(TRUE)
if(currentIndex<=process.len)
currentOverlay = process[currentIndex]

/*
If we're trying to generate a player's image, we do not want to include things like effects and whatnot.
We only want their gear, skin, and hair to show up.
*/

if(istype(A, /mob/player))
var/mob/player/P = A

if(!(currentOverlay:icon in P.flat_images))
currentIndex++
continue

currentLayer = currentOverlay:layer
if(currentLayer<0) // Special case for FLY_LAYER
ASSERT(currentLayer > -1000)
if(processSubset == 0) // Underlay
currentLayer = A.layer+currentLayer/1000
else // Overlay
currentLayer = A.layer+(1000+currentLayer)/1000

// Sort add into layers list
for(compareIndex=1,compareIndex<=layers.len,compareIndex++)
compareOverlay = layers[compareIndex]
if(currentLayer < layers[compareOverlay]) // Associated value is the calculated layer
layers.Insert(compareIndex,currentOverlay)
layers[currentOverlay] = currentLayer
break
if(compareIndex>layers.len) // Reached end of list without inserting
layers[currentOverlay]=currentLayer // Place at end

currentIndex++

if(currentIndex>process.len)
if(processSubset == 0) // Switch to overlays
currentIndex = 1
processSubset = 1
process = A.overlays
else // All done
break

if(cache!=0) // If cache is NOT disabled
// Create a hash value to represent this specific flattened icon
hash = "[parentColor],[scale]"
for(var/I in layers)
hash += "\ref[I:icon],[I:icon_state],[I:dir != SOUTH ? I:dir : dir],[I:pixel_x],[I:pixel_y],[I:color],[I:alpha];_;"

if(crops)
hash += "[crops[1]],[crops[2]],[crops[3]],[crops[4]]"

hash=md5(hash)

if(cache!=2) // If NOT overriding cache
// Check if the icon has already been generated
if((hash in _flatIcons) && _flatIcons[hash])
// Icon already exists, just return that one
return list(_flatIcons[hash], hash)

var
// We start with a blank canvas, otherwise some icon procs crash silently
icon/flat = icon('blank.dmi') // Final flattened icon
icon/add // Icon of overlay being added

// Set current dimensions of flattened icon
flatX1=1
flatX2=flat.Width()
flatY1=1
flatY2=flat.Height()

// Dimensions of overlay being added
addX1;addX2;addY1;addY2

for(var/I in layers)

add = icon(I:icon || A.icon
, I:icon_state || (I:icon && (A.icon_state in icon_states(I:icon)) && A.icon_state)
, (I:dir != SOUTH ? I:dir : dir)
, frame
, moving)

// Apply any color or alpha settings
if(I:color || I:alpha != 255)
var/rgba = (I:color || "#FFFFFF") + copytext(rgb(0,0,0,I:alpha), 8)
add.Blend(rgba, ICON_MULTIPLY)

if(parentColor)
add.Blend(parentColor, ICON_MULTIPLY)

// Find the new dimensions of the flat icon to fit the added overlay
addX1 = min(flatX1, I:pixel_x+1)
addX2 = max(flatX2, I:pixel_x+add.Width())
addY1 = min(flatY1, I:pixel_y+1)
addY2 = max(flatY2, I:pixel_y+add.Height())

if(addX1!=flatX1 || addX2!=flatX2 || addY1!=flatY1 || addY2!=flatY2)
// Resize the flattened icon so the new icon fits
flat.Crop(addX1-flatX1+1, addY1-flatY1+1, addX2-flatX1+1, addY2-flatY1+1)
flatX1=addX1;flatX2=addX2
flatY1=addY1;flatY2=addY2

// Blend the overlay into the flattened icon
flat.Blend(add,ICON_OVERLAY,I:pixel_x+2-flatX1,I:pixel_y+2-flatY1)

if(istype(A, /mob) && flat.Width() > 64 && flat.Height() > 80)
flat.Shift(WEST, 32)
flat.Shift(SOUTH, 32)
flat.Crop(0, 0, 63, 79)

if(crops)
flat.Crop(crops[1], crops[2], crops[3], crops[4])

if(cache!=0) // If cache is NOT disabled
// Cache the generated icon in our list so we don't have to regenerate it
_flatIcons[hash] = flat

if(scale > 1)
flat.Scale(flat.Width() * scale, flat.Height() * scale)

return list(flat, hash)


For the most part, these icons are not displayed on the map. They are displayed on the UI by generating a valid url with browse_rsc().
I don't think that would be likely to result in in-game slowdowns, since you're seeing this icon preload code happen mid-game. The fact that you're seeing that suggests to me that the issue is with a different dynamic icon.
Well, crap.

I found out that the IconInfo isn't processed at game load as I thought, because the message required to trigger this actually comes later. Which makes sense in hindsight. I could grab the alpha map during the initial load, for non-dynamic icons, and maybe that'd be enough.

I thought I'd try to get clever and offload alpha map proessing to another true thread via a WebWorker, but it turns out Dart's support in this area is really spotty; DOM-enabled isolates can't spawn other isolates that use dart:html, and dart:html inappropriately includes the ImageData class. So I can't use that option till they get their ducks in a row.

Until then, I think my best choice is going to be preloading the alpha maps at game load, for known icons, and let dynamic icons fall back on the old behavior where mouse clicks are concerned.