<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>winrickLabs Blog</title>
        <link>https://blog.winricklabs.com</link>
        <description>A detailed and to-the-point technology blog that relates to winrickLabs LLC</description>
        <lastBuildDate>Tue, 09 Dec 2025 00:00:00 GMT</lastBuildDate>
        <docs>http://blogs.law.harvard.edu/tech/rss</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2019, winrickLabs LLC</copyright>
        <item>
            <title><![CDATA[(12-9-2025) How to Find Flicker Free LED Christmas Lights]]></title>
            <link>https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html</link>
            <guid>https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html</guid>
            <pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[(12-9-2025) How to Find Flicker Free LED Christmas Lights - Size: 1.4 kB. 1 min read]]></description>
            <content:encoded><![CDATA[<html>
<head>
    <title>(12-9-2025) How to Find Flicker Free LED Christmas Lights | winrickLabs Blog</title>
    <link rel="shortcut icon" href="/images/favicon.png"/>
    <link rel="stylesheet" href="/css/pure-min.css">
    <link rel="stylesheet" href="/css/grids-responsive-min.css">
    <link rel="stylesheet" href="/css/blog.css">
    <script src="js/blog.min.js"></script>
    <script src="https://fastcomments.com/js/comment-ui.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script async src="https://devonsanalytics.xyz/js/watchly.min.js" data-key="5d1fcfabcc00a66e161816b4"></script>
    <link rel="alternate" type="application/rss+xml" title="WinrickLabs Blog" href="https://blog.winricklabs.com/rss.xml">
</head>
<body>
<div id="layout" class="pure-g">
    <div class="sidebar pure-u-1 pure-u-md-1-5">
        <div class="header">
            <h1 class="brand-title"><a href="/">winrickLabs</a></h1>
            <h2 class="brand-tagline">Blog</h2>
            <div class="buttons">
                <a href="https://blog.winricklabs.com/rss.xml"><img src="/images/rss.png"/></a>
                <a href='https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fblog.winricklabs.com%2Frss.xml'
                   target='blank'><img id='feedlyFollow'
                                       src='https://s3.feedly.com/img/follows/feedly-follow-rectangle-volume-medium_2x.png'
                                       alt='follow us in feedly' width='71' height='28'></a>
            </div>
        </div>
        <div class="sidebar-links">
                <div class="date">Tue Dec 09 2025</div>
                <a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html"> How to Find Flicker Free LED Christmas Lights</a>
                <div class="date">Sat May 13 2023</div>
                <a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html"> Reverse Engineering Netpanzer and Extracting Assets</a>
                <div class="date">Thu Apr 02 2020</div>
                <a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html"> Just How Fast is EJS</a>
                <div class="date">Mon Feb 17 2020</div>
                <a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures For MMO Game Backends in Java</a>
                <div class="date">Fri May 17 2019</div>
                <a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html"> Pixmap Release Launching Rewind</a>
                <div class="date">Sun May 05 2019</div>
                <a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html"> About This</a>
        </div>
    </div>

    <div class="content post-page pure-u-1 pure-u-md-3-4">
        <div>
            <div class="posts">
                <section class="post">
                    <div class="post-description">
                        <p>
                            <h6 id="postdate">Tue Dec 09 2025</h6>
<h1 id="a-hrefhttpsblogwinricklabscom12-9-2025-how-to-find-flicker-free-led-christmas-lightshtmlhow-to-find-flicker-free-led-christmas-lightsa"><a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html">How to Find Flicker Free LED Christmas Lights</a></h1>
<p>*1 min read*</p>
<p>Like many I am a bit sensitive to the 30hz flicker of cheap LED Christmas light strings. I&#39;ve even tried the
commercial &quot;low flicker&quot; sets, and they are better, but still have flicker that makes my eyes sore after some time.</p>
<p>What I have found is that the &quot;low voltage&quot; sets, for example 20 or 30v sets, usually have decent DC power supplies
with smoothing that do not flicker.</p>
<p><a href="https://amzn.to/4rWgTwl">Here is the set I bought 5 strings of this year that I like.</a></p>
<p>The set above worked well for me, and they look pretty decent. I considered Tru Tone bulbs, but they are not flicker free
as clarified by their own marketing. The durability of this set isn&#39;t the best, but I expect they will last 3-5 seasons just fine. I&#39;ve strung them
from the house to a tree nearby and no concerns they&#39;ll break or anything.</p>
<p>Good luck shopping this year, and hopefully we can reduce the amount of flickery houses during Christmas! :)</p>

                        </p>
                    </div>
                </section>
                <script src="https://fastcomments.com/js/embed-v2.min.js"></script>
                <div id="fastcomments-widget"></div>
                <script>
                    window.FastCommentsUI(document.getElementById('fastcomments-widget'), {
                        tenantId: 'StrTVey_yMpw'
                    });
                </script>
            </div>
        </div>

        <div class="footer">Copyright winrickLabs LLC 2019 - 2026</div>
    </div>
</div>
</body>
</html>
]]></content:encoded>
            <author>winrid+wlb @ gmail DOT com (Devon Winrick)</author>
        </item>
        <item>
            <title><![CDATA[(5-13-2023) Reverse Engineering Netpanzer and Extracting Assets]]></title>
            <link>https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html</link>
            <guid>https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html</guid>
            <pubDate>Sat, 13 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[(5-13-2023) Reverse Engineering Netpanzer and Extracting Assets - Size: 15.5 kB. 10 min read]]></description>
            <content:encoded><![CDATA[<html>
<head>
    <title>(5-13-2023) Reverse Engineering Netpanzer and Extracting Assets | winrickLabs Blog</title>
    <link rel="shortcut icon" href="/images/favicon.png"/>
    <link rel="stylesheet" href="/css/pure-min.css">
    <link rel="stylesheet" href="/css/grids-responsive-min.css">
    <link rel="stylesheet" href="/css/blog.css">
    <script src="js/blog.min.js"></script>
    <script src="https://fastcomments.com/js/comment-ui.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script async src="https://devonsanalytics.xyz/js/watchly.min.js" data-key="5d1fcfabcc00a66e161816b4"></script>
    <link rel="alternate" type="application/rss+xml" title="WinrickLabs Blog" href="https://blog.winricklabs.com/rss.xml">
</head>
<body>
<div id="layout" class="pure-g">
    <div class="sidebar pure-u-1 pure-u-md-1-5">
        <div class="header">
            <h1 class="brand-title"><a href="/">winrickLabs</a></h1>
            <h2 class="brand-tagline">Blog</h2>
            <div class="buttons">
                <a href="https://blog.winricklabs.com/rss.xml"><img src="/images/rss.png"/></a>
                <a href='https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fblog.winricklabs.com%2Frss.xml'
                   target='blank'><img id='feedlyFollow'
                                       src='https://s3.feedly.com/img/follows/feedly-follow-rectangle-volume-medium_2x.png'
                                       alt='follow us in feedly' width='71' height='28'></a>
            </div>
        </div>
        <div class="sidebar-links">
                <div class="date">Tue Dec 09 2025</div>
                <a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html"> How to Find Flicker Free LED Christmas Lights</a>
                <div class="date">Sat May 13 2023</div>
                <a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html"> Reverse Engineering Netpanzer and Extracting Assets</a>
                <div class="date">Thu Apr 02 2020</div>
                <a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html"> Just How Fast is EJS</a>
                <div class="date">Mon Feb 17 2020</div>
                <a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures For MMO Game Backends in Java</a>
                <div class="date">Fri May 17 2019</div>
                <a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html"> Pixmap Release Launching Rewind</a>
                <div class="date">Sun May 05 2019</div>
                <a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html"> About This</a>
        </div>
    </div>

    <div class="content post-page pure-u-1 pure-u-md-3-4">
        <div>
            <div class="posts">
                <section class="post">
                    <div class="post-description">
                        <p>
                            <h6 id="postdate">Sat May 13 2023</h6>
<h1 id="a-hrefhttpsblogwinricklabscom5-13-2023-reverse-engineering-netpanzer-and-extracting-assetshtmlreverse-engineering-netpanzer-and-extracting-assetsa"><a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html">Reverse Engineering Netpanzer and Extracting Assets</a></h1>
<p><em>Article size is 15.5 kB and is a 10 min read</em></p>
<p>A free game I used to love to play around 2006 was NetPanzer. The only machines I had access to were free Pentium III machines,
which I threw Linux on, so that kinda limited me to games like NetPanzer and WolfET.</p>
<p>This was also the game where, in my young youth, learned through the chat feature how much people hated Americans :)</p>
<p>Recently I&#39;d been fixing bugs and stuff in WolfET (a little in 2014, and then again recently), and wondered what was up
with NetPanzer.</p>
<h2 id="the-current-state-of-things">The Current State of Things</h2>
<p>The game, today, does not run. If you install NetPanzer from your repo&#39;s package manager and start it, it will crash.</p>
<p>If you run it from the shell, you&#39;ll see that&#39;s because it can&#39;t reach the master server, and then it segfaults.</p>
<p><code>netpanzer.info</code> is no longer online in 2023. I found an archived forum post from 2019 of a maintainer asking for help
with the project. It seems soon after, when nobody replied, the project slowly died and went offline.</p>
<p>UPDATE May 9th: It seems after bugging the maintainer, about a month later, netpanzer.info is back online, and they restored the master
server. So you can play NetPanzer again! I am also working with the maintainer to run a second master server. I&#39;ve already thrown up one 0.8.x
dedicated server that supports up to 100 players.</p>
<h2 id="a-new-map-builder">A New Map Builder?</h2>
<p>One of the issues with the existing game is that, while you can run your own server, you can&#39;t create new maps.</p>
<p>The map building tool is long gone, and the asset storage is not immediately obvious. So how do we go about fixing that? Custom maps
allow people to mod the game.</p>
<p>One idea I&#39;ve been floating around is adding the ability to build and import maps from an existing popular tool. So let&#39;s see
if we can figure out how the game assets are managed.</p>
<h1 id="extracting-the-assets">Extracting The Assets</h1>
<p>The first assets that are easy to extract are the game flags and icons, which are in BMP format. This gave an early
clue as to how the game assets are stored elsewhere - as raw, uncompressed, bitmaps.</p>
<p>We find some interesting files in a <code>wads</code> folder:</p>
<ul>
<li><code>wads/netp.act</code></li>
<li><code>wads/netpmenu.act</code></li>
<li><code>wads/summar12mb.tls</code></li>
</ul>
<p>... and in <code>units/pics/pak</code> we find many files ending in <code>.pak</code> files, with the unit names (like <code>ArchHNSD.pak</code>).</p>
<p>Games of this era commonly stored the assets in &quot;.pak&quot; files, but they all used their own format, the exception being IDTech 3 engine
games (like WolfET) which all used the same format designed by John Carmack.</p>
<p>NetPanzer is not an IDTech 3 game, and uses a custom binary format.</p>
<p>So let&#39;s reverse engineer the pak files.</p>
<h2 id="the-different-file-types">The Different File Types</h2>
<p>There are a few binary formats.</p>
<p>The <code>.pak</code> files are purely a header in binary, with the bitmap image data after.</p>
<p>To read the files, we simply seek through X number of bytes at a time:</p>
<pre><code>const unit_contents = fs.readFileSync(&#39;units/pics/pak/TitaHNSD.pak&#39;);
const unit_buffer = SmartBuffer.fromBuffer(unit_contents);
console.log(&#39;unit version&#39;, unit_buffer.readInt32LE());
console.log(&#39;unit x&#39;, unit_buffer.readInt32LE());
console.log(&#39;unit y&#39;, unit_buffer.readInt32LE());
console.log(&#39;unit frame_count&#39;, unit_buffer.readInt32LE());
console.log(&#39;unit fps&#39;, unit_buffer.readInt32LE());
console.log(&#39;offset_x&#39;, unit_buffer.readInt32LE());
console.log(&#39;offset_y&#39;, unit_buffer.readInt32LE());</code></pre><p>Whereas, the unit metadata is stored in <code>/profiles</code> as text:</p>
<pre><code>hitpoints =  120;
attack =  350;
reload =  110;
range =  48;
regen =  90;
defend_range =  119;
speed_rate =  16;
speed_factor =  2;
image =  &quot;pics/menus/vehicleSelectionView/archer.bmp&quot;
bodysprite =  &quot;units/pics/pak/ArchHNSD.pak&quot;
bodyshadow =  &quot;units/pics/pak/ArchHSSD.pak&quot;
turretsprite =  &quot;units/pics/pak/ArchTNSD.pak&quot;
turretshadow =  &quot;units/pics/pak/ArchTSSD.pak&quot;
soundselected =  &quot;mu-selected&quot;
soundfire =  &quot;mu-fire&quot;
weapon =  &quot;DOUBLEMISSILE&quot;
boundbox =  40</code></pre><p>So what about the <code>.act</code> and <code>.tls</code> files?</p>
<p>Well, the <code>.act</code> files are color palettes. It turns out that the pak files, and the <code>.tls</code> files, simply store offsets
into the <code>.act</code> files. The reduces the amount of data stored in each map and unit file. We simply store the colors once, and can
use a single 8 bit integer to index into one of the <code>256</code> colors to save disk space.</p>
<h2 id="extracting-game-units">Extracting Game Units</h2>
<p>Extracting the game unit assets was quite easy. A bit of time was spent doing this myself, and then I found a file called
<code>pak2bmp.cpp</code> in the original code</p>
<p>First was to figure out the build system, which uses <code>scons</code>. <code>scons</code> uses a Python file to define the build process, like so:</p>
<pre><code>def globSources(localenv, sourcePrefix, sourceDirs, pattern):
    sources = []
    sourceDirs = Split(sourceDirs)
    for d in sourceDirs:
        sources.append(glob.glob( sourcePrefix + &#39;/&#39; + d + &#39;/&#39; + pattern))
    sources = Flatten(sources)
    targetsources = []
    for s in sources:
        targetsources.append(buildpath + s)

    return targetsources</code></pre><p>The build script is a ~280 line Python script. After installing <code>scons</code> the build promptly fails with <code>print not found</code> - as the
file was written for Python 2. Luckily converting it to Python 3 was quite easy, so onward...</p>
<p>I have no idea how to change the default build target with scons to just build our <code>pak2bmp.cpp</code> tool. Eventually I realized I can
just change the default in the <code>SContruct</code> file:</p>
<pre><code>pak2bmp = env.Program( binpath+&#39;pak2bmp&#39;+exeappend, &#39;support/tools/pak2bmp.cpp&#39;)
Alias(&#39;pak2bmp&#39;,pak2bmp)

bmp2pak = env.Program( binpath+&#39;bmp2pak&#39;+exeappend, &#39;support/tools/bmp2pak.cpp&#39;)
Alias(&#39;bmp2pak&#39;,bmp2pak)

Default(map2bmp)</code></pre><p>This worked, however I ran into compilation issues with how the codebase was extending <code>std::exception</code> for the networking layer (I would later fix this, but that&#39;s for a separate post).</p>
<p>Eventually I decided - we don&#39;t need Networking - so after learning <code>scons</code> some more I simply omitted all the networking code from the build.</p>
<pre><code>$ ./pak2bmp 
pak2bmp for NetPanzer V 1.1
use: ./pak2bmp &lt;filename&gt; &lt;output_folder&gt; [palette_file_optional]
note: if using palette file, use only the name without extension or path
note2: even on windows the path must be separated by &#39;/&#39;
note3: YOU have to create the output directory
example for using palete (default is netp):
./pak2bmp units/pics/pak/TitaHNSD.pak titan_body netp</code></pre><p>Success!</p>
<p>We can now extract all the unit assets with a little script:</p>
<pre><code>ls units/pics/pak | xargs -i sh -c &#39;mkdir ./bmp/units/pics/{}&#39;
ls units/pics/pak/ | xargs -i sh -c &#39;./pak2bmp units/pics/pak/{} ./bmp/units/pics/{}/&#39;</code></pre><p>The unit assets are split up into three categories for each unit:</p>
<ul>
<li>The unit body, with a frame for each rotation.</li>
<li>The unit turret, with a frame for each rotation.</li>
<li>The unit shadow, again with many frames.</li>
</ul>
<p>So while in game they look like:</p>
<div class="text-center">
    <a href="images/netpanzer-units.png">
    <img 
            src="images/netpanzer-units.png"
            alt="NetPanzer Units"
            title="NetPanzer Units" />
    </a>
</div>

<p>They are actually stored like:</p>
<div class="text-center">
    <a href="images/netpanzer-units-raw.png">
    <img 
            src="images/netpanzer-units-raw.png"
            alt="NetPanzer Units Stored"
            title="NetPanzer Units Stored" />
    </a>
</div>

<h2 id="extracting-game-map">Extracting Game Map</h2>
<p>Map data. This is the hard part. There is no &quot;dump the map data&quot; script.</p>
<p>It turns out the actual tile images are stored in <code>summer12mb.tls</code>. As you could guess, this is a <strong>12mb</strong> file.
The actual maps are quite small, because they are just pointers into offsets into this file.</p>
<p>It&#39;s quite a bit more complicated than that, however. I was hoping I could just get a list of bytes, after figuring out the
pointers into the palette, and dump it to a bit bitmap file for each map, and then convert this file to another more modern format.</p>
<p>It was not going to be quite this simple. :)</p>
<p>The map data is split across a few structures.</p>
<p>First, the <code>.tls</code> file itself starts with:</p>
<pre><code>class TILE_DBASE_HEADER
{
public:
    unsigned char    netp_id_header[64];
    unsigned short    version;
    unsigned short    x_pix;
    unsigned short    y_pix;
    unsigned short    tile_count;
    unsigned char    palette[768];
}</code></pre><p>The map tiles also have a list of this structure, which stores the <code>move_value</code> (how fast a unit can move, if at all. The game uses A*):</p>
<pre><code>class TILE_HEADER
{
public:
    char    attrib;
    char    move_value;
    char    avg_color;
}</code></pre><p>...and finally a uint8 of map_data.</p>
<p>Eventually I figured out how the game loads and renders maps, so I could stop reverse engineering the structure and create my own
<code>map2bmp.cpp</code>:</p>
<p>First, we need to &quot;start the map loading&quot;, which takes a map name, whether or not to load the map tiles, and the
number of partitions to load it into:</p>
<pre><code>MapInterface::startMapLoad(filename.c_str(), true, 1);</code></pre><p>Then there is a function we are supposed to repeatedly call to load the map, which takes a pointer to an integer
that is updated to track the progress:</p>
<pre><code>int percent_complete;
char strbuf[256];

while( MapInterface::loadMap( &amp;percent_complete ) == true ) {
    sprintf( strbuf, &quot;Loading Game Data ... (%d%%)&quot;, percent_complete);
}

printf(&quot;Map loaded!\n&quot;);</code></pre><p>Now let&#39;s create an SDL1 <code>Surface</code> to write the map to:</p>
<pre><code>int total_width = MapInterface::getMap()-&gt;getWidth();
int total_height = MapInterface::getMap()-&gt;getHeight();
Surface unpacked(total_width, total_height, 1);

SDL_Surface *surf = SDL_CreateRGBSurfaceFrom(unpacked.getFrame0(),
                                              unpacked.getWidth(),
                                             unpacked.getHeight(),
                                             8,
                                             unpacked.getPitch(), // pitch
                                             0,0,0,0);


if ( ! surf )
{
    printf(&quot;surface is null!&quot;);
}</code></pre><p>Load our palette and tell SDL about it:</p>
<pre><code>Palette::loadACT(palettefile);
SDL_SetColors(surf, Palette::color, 0, 256);</code></pre><p>Now we can clear our packed surface and write the tiles:</p>
<pre><code>unpacked.fill(0);
for (int x = 0; x &lt; MapInterface::getMap()-&gt;getWidth(); x++) {
    for (int y = 0; y &lt; MapInterface::getMap()-&gt;getHeight(); y++) {
//        pak.setFrame(n);
        blitTile(unpacked, MapInterface::getMap()-&gt;getValue(x, y), x + 32, y + 32);
    }
}</code></pre><p>The <code>blitTile</code> function was not easily accessible, as I could not get enough of the project to compile as the time to use it. Luckily
we can just copy it into our script:</p>
<pre><code>void blitTile(Surface &amp;dest, unsigned short tile, int x, int y)
{
    PIX * tileptr = TileInterface::getTileSet()-&gt;getTile(tile);

    int lines = 32;
    int columns = 32;

    if ( y &lt; 0 )
    {
        lines = 32 + y;
        tileptr += ((32-lines)*32);
        y = 0;
    }

    if ( x &lt; 0 )
    {
        columns = 32 + x;
        tileptr += (32-columns); // advance the unseen pixels
        x = 0;
    }

    PIX * destptr = dest.getFrame0();
    destptr += (y * dest.getPitch()) + x;


    if ( y + 32 &gt; (int)dest.getHeight() )
    {
        lines = (int)dest.getHeight() - y;
    }

    if ( x + 32 &gt; (int)dest.getWidth())
    {
        columns = (int)dest.getWidth() - x;
    }

    PIX * endptr = destptr + (lines * dest.getPitch());

    for ( /* nothing */ ; destptr &lt; endptr; destptr += dest.getPitch())
    {
        memcpy(destptr,tileptr,columns);
        tileptr +=32;
    }

}</code></pre><p>This is quite a fun piece of code because it uses pointer arithmetic. Instead of working with an array, we simply get
an address of memory and offset the address as we copy the memory to our surface based on the &quot;pitch&quot; of bytes in the format, which
is number of bytes in between &quot;rows&quot;. The array syntax in C is just sugar for doing this (why we don&#39;t use the sugar I don&#39;t know).</p>
<p>This is the final version of the code. It took a few iterations to get it right:</p>
<div class="text-center">
    <a href="images/netpanzer-map-attempt-1.png">
    <img 
            src="images/netpanzer-map-attempt-1.png"
            alt="Bad Neuburg Extracted Badly (#1)"
            title="Bad Neuburg Extracted Badly (#1)" />
    </a>
</div>

<p>Our second attempt, a very blurry map:</p>
<div class="text-center">
    <a href="images/netpanzer-map-attempt-2.png">
    <img 
            src="images/netpanzer-map-attempt-2.png"
            alt="Bad Neuburg Extracted Badly (#2)"
            title="Bad Neuburg Extracted Badly (#2)" />
    </a>
</div>

<p>Then a higher quality map that is obviously messed up:</p>
<div class="text-center">
    <a href="images/netpanzer-map-attempt-3.png">
    <img 
            src="images/netpanzer-map-attempt-3.png"
            alt="Bad Neuburg Extracted Badly (#3)"
            title="Bad Neuburg Extracted Badly (#3)" />
    </a>
</div>

<p>... and finally! The resulting image is uncompressed 11k x 11k pixels. It&#39;s 121 megabytes as a BMP file, or 47mb as a webp with no percieved loss of quality.</p>
<p>Click to enlarge.</p>
<div class="text-center">
    <a href="images/netpanzer-bad-neuburg-full.webp">
    <img 
            src="images/netpanzer-bad-neuburg-cover.png"
            alt="NetPanzer Bad Neuburg Extracted"
            title="NetPanzer Bad Neuburg Extracted" />
    </a>
</div>

<h2 id="conclusion">Conclusion</h2>
<p>I&#39;ll be happy to see a new community form around the game if we can add map creation and improve
the hosting story around the server, and maybe some central stats/prestige tracking.</p>
<p>It&#39;s neat to see what they did to compress the assets, whereas today we might just end up with a &quot;tiny&quot; 1GB game on steam. :)</p>

                        </p>
                    </div>
                </section>
                <script src="https://fastcomments.com/js/embed-v2.min.js"></script>
                <div id="fastcomments-widget"></div>
                <script>
                    window.FastCommentsUI(document.getElementById('fastcomments-widget'), {
                        tenantId: 'StrTVey_yMpw'
                    });
                </script>
            </div>
        </div>

        <div class="footer">Copyright winrickLabs LLC 2019 - 2026</div>
    </div>
</div>
</body>
</html>
]]></content:encoded>
            <author>winrid+wlb @ gmail DOT com (Devon Winrick)</author>
        </item>
        <item>
            <title><![CDATA[(04-02-2020) Just How Fast is EJS]]></title>
            <link>https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html</link>
            <guid>https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html</guid>
            <pubDate>Thu, 02 Apr 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[(04-02-2020) Just How Fast is EJS - Size: 20.5 kB. 13 min read]]></description>
            <content:encoded><![CDATA[<html>
<head>
    <title>(04-02-2020) Just How Fast is EJS | winrickLabs Blog</title>
    <link rel="shortcut icon" href="/images/favicon.png"/>
    <link rel="stylesheet" href="/css/pure-min.css">
    <link rel="stylesheet" href="/css/grids-responsive-min.css">
    <link rel="stylesheet" href="/css/blog.css">
    <script src="js/blog.min.js"></script>
    <script src="https://fastcomments.com/js/comment-ui.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script async src="https://devonsanalytics.xyz/js/watchly.min.js" data-key="5d1fcfabcc00a66e161816b4"></script>
    <link rel="alternate" type="application/rss+xml" title="WinrickLabs Blog" href="https://blog.winricklabs.com/rss.xml">
</head>
<body>
<div id="layout" class="pure-g">
    <div class="sidebar pure-u-1 pure-u-md-1-5">
        <div class="header">
            <h1 class="brand-title"><a href="/">winrickLabs</a></h1>
            <h2 class="brand-tagline">Blog</h2>
            <div class="buttons">
                <a href="https://blog.winricklabs.com/rss.xml"><img src="/images/rss.png"/></a>
                <a href='https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fblog.winricklabs.com%2Frss.xml'
                   target='blank'><img id='feedlyFollow'
                                       src='https://s3.feedly.com/img/follows/feedly-follow-rectangle-volume-medium_2x.png'
                                       alt='follow us in feedly' width='71' height='28'></a>
            </div>
        </div>
        <div class="sidebar-links">
                <div class="date">Tue Dec 09 2025</div>
                <a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html"> How to Find Flicker Free LED Christmas Lights</a>
                <div class="date">Sat May 13 2023</div>
                <a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html"> Reverse Engineering Netpanzer and Extracting Assets</a>
                <div class="date">Thu Apr 02 2020</div>
                <a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html"> Just How Fast is EJS</a>
                <div class="date">Mon Feb 17 2020</div>
                <a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures For MMO Game Backends in Java</a>
                <div class="date">Fri May 17 2019</div>
                <a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html"> Pixmap Release Launching Rewind</a>
                <div class="date">Sun May 05 2019</div>
                <a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html"> About This</a>
        </div>
    </div>

    <div class="content post-page pure-u-1 pure-u-md-3-4">
        <div>
            <div class="posts">
                <section class="post">
                    <div class="post-description">
                        <p>
                            <h6 id="postdate">Thu Apr 02 2020</h6>
<h1 id="a-hrefhttpsblogwinricklabscom04-02-2020-just-how-fast-is-ejshtmljust-how-fast-is-ejsa"><a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html">Just How Fast is EJS?</a></h1>
<p><em>Article size is 20.5 kB and is a 13 min read</em></p>
<p>With one project most of our server-side rendered pages are served using the EJS ExpressJS plugin. Since
we&#39;re using NodeJS this makes sense for us. EJS is fast, and easy to write. The amount of abstraction is also low since the library is fairly small, and I think everyone
knows how big of a fan I am of the right (and less) abstractions.</p>
<p>For this project, EJS works well for the marketing site and administration app with pages being able to be rendered in single-digit milliseconds. But what about the really heavily
hit stuff - the embedded widgets this project serves?</p>
<h4 id="quick-background---how-the-embedded-widgets-work">Quick Background - How The Embedded Widgets Work</h4>
<p>For the embedded widgets, a script is loaded, which injects a styled and dynamically resized iframe into the page.</p>
<p>The script (#1) is needed to control the iframe - its styling, the auto resizing, etc.</p>
<p>The /embed endpoint (#2) loads our iframe content.</p>
<p>So what we&#39;re talking about here is #2.</p>
<h4 id="just-cache-it-dude">Just cache it dude</h4>
<p>The problem with caching in this scenario is when items fall out of the cache. For this project we don&#39;t want to risk
the 1% percentile latency being bad. So that /embed endpoint should always be fast.</p>
<p>So let&#39;s benchmark.</p>
<h4 id="benchmark---template-literal">Benchmark - Template Literal</h4>
<p>First let&#39;s get a baseline. Let&#39;s compare EJS to the fastest thing we can - returning a string. No caching, although in production currently we use an LRU cache.</p>
<p>What if the endpoint was just a string literal?</p>
<p>We&#39;ll use autocannon with the default settings to run our test, using and endpoint with actual parameters taken from production.</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬─────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max     │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼─────────┤
│ Latency │ 2 ms │ 2 ms │ 4 ms  │ 5 ms │ 2.25 ms │ 0.69 ms │ 15.7 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴─────────┘
┌───────────┬─────────┬─────────┬────────┬─────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%    │ 97.5%   │ Avg     │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 2175    │ 2175    │ 3869   │ 4001    │ 3631    │ 555.19 │ 2174    │
├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 3.54 MB │ 3.54 MB │ 6.3 MB │ 6.52 MB │ 5.91 MB │ 904 kB │ 3.54 MB │
└───────────┴─────────┴─────────┴────────┴─────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
36k requests in 10.08s, 59.1 MB read
</div>

<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 3 ms  │ 3 ms │ 2.08 ms │ 0.31 ms │ 10.98 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 3755    │ 3755    │ 3945    │ 4015    │ 3920.82 │ 79.38  │ 3755    │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 6.12 MB │ 6.12 MB │ 6.43 MB │ 6.54 MB │ 6.39 MB │ 129 kB │ 6.12 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
43k requests in 11.08s, 70.2 MB read
</div>

<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 3 ms  │ 3 ms │ 2.06 ms │ 0.28 ms │ 11.24 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 3887    │ 3887    │ 3965    │ 4033    │ 3969.73 │ 41.4    │ 3887    │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 6.33 MB │ 6.33 MB │ 6.46 MB │ 6.57 MB │ 6.47 MB │ 66.9 kB │ 6.33 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
44k requests in 11.08s, 71.1 MB read
</div>

<p>You can see it gets faster each time as we make Turbofan happy and the JIT kicks in.</p>
<p>We can see that after JIT compilation our max latency is 11.24ms with the lowest requests/second over ten seconds being 3887.</p>
<p>On kind of unrelated tangent, I wanted to show something real quick. Logging in NodeJS is slow, even with the best of frameworks. How slow?</p>
<p>Well, using connect w/ log4js, logging a line like this with each request:</p>
<div class="code">
[2020-04-02T00:04:05.642] [DEBUG] express - ::1 - - "GET /embed?config=%7B%22tenantId%22%3A%22DTGXwVKr%22%2C%22apiHost%22%3A%22http%253A%252F%252Flocalhost%253A3001%22%2C%22urlId%22%3A%22http%253A%252F%252Flocalhost%253A63342%252Fwatchly-models%252Fscripts%252Flocal-test-lifetime.html%253F_ijt%253Dlh9s0h0fgv1t2n2ceibdot1c1g%22%7D HTTP/1.1" 200 1065 "http://localhost:63342/watchly-models/scripts/local-test-lifetime.html?_ijt=lh9s0h0fgv1t2n2ceibdot1c1g" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
</div>

<p>It has an impact - surprisingly. So let&#39;s disable logging for a second.</p>
<p>I ran it twice, here&#39;s the second run.</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 1 ms │ 1 ms │ 2 ms  │ 3 ms │ 1.15 ms │ 0.44 ms │ 13.68 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 5051    │ 5051    │ 5467    │ 5623    │ 5429.28 │ 172.67 │ 5049    │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 7.78 MB │ 7.78 MB │ 8.42 MB │ 8.67 MB │ 8.36 MB │ 266 kB │ 7.78 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
60k requests in 11.08s, 92 MB read
</div>

<p>Wow! Our max latency isn&#39;t much better, but our average is cut in half, and we go from 44k requests/second to 60.</p>
<p>Of course, Node has to take a lock to write to stdout since you&#39;ll have many workers writing logs, you don&#39;t want one log to be
half written into another one. So we have a concurrency bottleneck with logging every request even if you have many workers.</p>
<p>Note that the impact is so profound just because what we&#39;re testing is so light, but it can build up. Generally &quot;aggregated logging&quot; or batched logs sent over the network are better
for performance focused systems.</p>
<h4 id="benchmark---ejs">Benchmark - EJS</h4>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg    │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼────────┼─────────┼──────────┤
│ Latency │ 4 ms │ 4 ms │ 7 ms  │ 8 ms │ 4.4 ms │ 0.94 ms │ 19.83 ms │
└─────────┴──────┴──────┴───────┴──────┴────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%  │ Avg     │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 1327    │ 1327    │ 2223    │ 2269   │ 2072.73 │ 286.91 │ 1327    │
├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 1.81 MB │ 1.81 MB │ 3.03 MB │ 3.1 MB │ 2.83 MB │ 391 kB │ 1.81 MB │
└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
23k requests in 11.08s, 31.1 MB read
</div>

<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 4 ms │ 4 ms │ 5 ms  │ 5 ms │ 4.11 ms │ 0.37 ms │ 12.36 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 2079    │ 2079    │ 2225    │ 2259    │ 2201.73 │ 57.87   │ 2078    │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.83 MB │ 2.83 MB │ 3.04 MB │ 3.08 MB │ 3 MB    │ 79.2 kB │ 2.83 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
24k requests in 11.08s, 33 MB read
</div>

<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 4 ms │ 4 ms │ 5 ms  │ 5 ms │ 4.07 ms │ 0.31 ms │ 13.22 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%  │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 2173    │ 2173    │ 2239    │ 2271   │ 2240.6  │ 28.83   │ 2173    │
├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.97 MB │ 2.97 MB │ 3.05 MB │ 3.1 MB │ 3.06 MB │ 38.9 kB │ 2.96 MB │
└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
22k requests in 10.07s, 30.6 MB read
</div>

<p>Wow, a huge difference. We can only serve half the requests in the same amount of time and latency is doubled across the board.</p>
<p>Again remember - it may seem crazy to optimize this. But for the nature of this project - a widget hosted on a third party page - can mean
that we get flooded with tons of requests for uncached pages at once. Even with autoscaling this could cause an outage or at least a hiccup.
Being able to handle twice the uncached traffic in a spike is a big deal.</p>
<p>So let&#39;s take a step back. What if we also add an LRU cache to our template literal?</p>
<p>First it starts off not so great, since the JIT hasn&#39;t warmed up yet.</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 4 ms  │ 5 ms │ 2.24 ms │ 0.72 ms │ 24.13 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬────────┬────────┬─────────┬─────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min     │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 2145   │ 2145   │ 3897    │ 4087    │ 3659    │ 546.44 │ 2145    │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 3.5 MB │ 3.5 MB │ 6.35 MB │ 6.66 MB │ 5.96 MB │ 890 kB │ 3.49 MB │
└───────────┴────────┴────────┴─────────┴─────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
40k requests in 11.07s, 65.6 MB read
</div>

<p>But then!</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev  │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 3 ms  │ 3 ms │ 2.06 ms │ 0.3 ms │ 11.41 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 3849    │ 3849    │ 3967    │ 4037    │ 3970.8  │ 53.39   │ 3848    │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 6.27 MB │ 6.27 MB │ 6.46 MB │ 6.58 MB │ 6.47 MB │ 86.9 kB │ 6.27 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
40k requests in 10.08s, 64.7 MB read
</div>

<p>Wait, isn&#39;t that the same as our string literal test? Let&#39;s check again.</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 3 ms  │ 3 ms │ 2.08 ms │ 0.36 ms │ 11.58 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬────────┬────────┬─────────┬─────────┬─────────┬────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min    │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤
│ Req/Sec   │ 3699   │ 3699   │ 3975    │ 4051    │ 3932.28 │ 96.96  │ 3699   │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤
│ Bytes/Sec │ 5.7 MB │ 5.7 MB │ 6.13 MB │ 6.25 MB │ 6.06 MB │ 150 kB │ 5.7 MB │
└───────────┴────────┴────────┴─────────┴─────────┴─────────┴────────┴────────┘
Req/Bytes counts sampled once per second.
43k requests in 11.08s, 66.6 MB read
</div>

<p>Yup. Just res.send(<code>${some} stuff</code>) is faster than checking if an item is in the cache and sending it back in the case of our
micro benchmark.</p>
<p>Just for giggles, what if we just used a regular map for the cache instead of this fancy lru cache?</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev  │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 3 ms  │ 3 ms │ 2.06 ms │ 0.3 ms │ 11.84 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%  │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 3779    │ 3779    │ 3977    │ 4023   │ 3961.73 │ 62.58   │ 3779    │
├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 5.82 MB │ 5.82 MB │ 6.13 MB │ 6.2 MB │ 6.1 MB  │ 96.6 kB │ 5.82 MB │
└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
44k requests in 11.08s, 67.1 MB read
</div>

<p>At this point what we are seeing is that most of the time is not spent actually constructing the value, but to actually enqueue it into the event loop
and send it over the wire.</p>
<p>So where do we go from here? Well, aside from making major architecture changes, let&#39;s revisit that logging thing. What if we disable logging for just that route?
It&#39;s not a big deal - we have the logs in Nginx.</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 0 ms │ 1 ms │ 1 ms  │ 1 ms │ 0.69 ms │ 0.49 ms │ 11.32 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬───────┬───────┬─────────┬─────────┬─────────┬────────┬───────┐
│ Stat      │ 1%    │ 2.5%  │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min   │
├───────────┼───────┼───────┼─────────┼─────────┼─────────┼────────┼───────┤
│ Req/Sec   │ 9103  │ 9103  │ 9447    │ 9663    │ 9438.91 │ 147.37 │ 9102  │
├───────────┼───────┼───────┼─────────┼─────────┼─────────┼────────┼───────┤
│ Bytes/Sec │ 14 MB │ 14 MB │ 14.6 MB │ 14.9 MB │ 14.5 MB │ 227 kB │ 14 MB │
└───────────┴───────┴───────┴─────────┴─────────┴─────────┴────────┴───────┘
Req/Bytes counts sampled once per second.
104k requests in 11.09s, 160 MB read
</div>

<p>Wow! Double the requests, half the average latency. But why?
I originally thought the reason is that this request has a giant query string that gets logged. Observe - if we remove that config from the query string and keep the request logs:</p>
<div class="code">┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 2 ms │ 2 ms │ 3 ms  │ 3 ms │ 2.04 ms │ 0.25 ms │ 11.12 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec   │ 4035    │ 4035    │ 4347    │ 4423    │ 4324.64 │ 103.49 │ 4034    │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 4.68 MB │ 4.68 MB │ 5.04 MB │ 5.13 MB │ 5.02 MB │ 119 kB │ 4.68 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
48k requests in 11.08s, 55.2 MB read
</div>

<p>A little better, but not much! Logging, and express middleware, is just slow.</p>
<p>I think what we are going to do is what bought us the lowest latency - rewriting in Rust! Just kidding. Here:</p>
<ol>
<li>Be careful caching in NodeJS. Thrashes GC, takes longer to get from cache than to reconstruct string after JIT.</li>
<li>Don&#39;t log these requests in the Node layer.</li>
<li>Template literals.</li>
<li>Cache at the proxy layer.</li>
<li>COMMENT YOUR OPTIMIZATIONS</li>
</ol>
<p>I wouldn&#39;t apply these optimizations to other things, but really we&#39;re desperate to make sure this project is fast as it can be in certain areas. Engineering is all
about trade-offs. Also, be a good developer. If you&#39;re going to write code that&#39;s tough to deal with, explain why:</p>
<div class="code">worker.use('/embed', require('./routes/embed')); // OPTIMIZATION This is here so we don't log the requests, which is slow.
worker.use(express.static(path.join(__dirname, 'public'))); // OPTIMIZATION This is here so we don't log the requests, which is slow.
worker.use(log4js.connectLogger(logger, { level: 'debug' }));
</div>

<p>Also, interestingly having the express.static router first slows things down by about 30%.</p>
<p>We could also probably get better performance out of optimizing a some code returned by the /embed endpoint, but that&#39;s for another day. :)</p>

                        </p>
                    </div>
                </section>
                <script src="https://fastcomments.com/js/embed-v2.min.js"></script>
                <div id="fastcomments-widget"></div>
                <script>
                    window.FastCommentsUI(document.getElementById('fastcomments-widget'), {
                        tenantId: 'StrTVey_yMpw'
                    });
                </script>
            </div>
        </div>

        <div class="footer">Copyright winrickLabs LLC 2019 - 2026</div>
    </div>
</div>
</body>
</html>
]]></content:encoded>
            <author>winrid+wlb @ gmail DOT com (Devon Winrick)</author>
        </item>
        <item>
            <title><![CDATA[(02-17-2020) - Efficient Data Structures For MMO Game Backends in Java]]></title>
            <link>https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html</link>
            <guid>https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html</guid>
            <pubDate>Mon, 17 Feb 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[(02-17-2020) - Efficient Data Structures For MMO Game Backends in Java - Size: 14.4 kB - 18.6 MB. 11 min read]]></description>
            <content:encoded><![CDATA[<html>
<head>
    <title>(02-17-2020) - Efficient Data Structures For MMO Game Backends in Java | winrickLabs Blog</title>
    <link rel="shortcut icon" href="/images/favicon.png"/>
    <link rel="stylesheet" href="/css/pure-min.css">
    <link rel="stylesheet" href="/css/grids-responsive-min.css">
    <link rel="stylesheet" href="/css/blog.css">
    <script src="js/blog.min.js"></script>
    <script src="https://fastcomments.com/js/comment-ui.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script async src="https://devonsanalytics.xyz/js/watchly.min.js" data-key="5d1fcfabcc00a66e161816b4"></script>
    <link rel="alternate" type="application/rss+xml" title="WinrickLabs Blog" href="https://blog.winricklabs.com/rss.xml">
</head>
<body>
<div id="layout" class="pure-g">
    <div class="sidebar pure-u-1 pure-u-md-1-5">
        <div class="header">
            <h1 class="brand-title"><a href="/">winrickLabs</a></h1>
            <h2 class="brand-tagline">Blog</h2>
            <div class="buttons">
                <a href="https://blog.winricklabs.com/rss.xml"><img src="/images/rss.png"/></a>
                <a href='https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fblog.winricklabs.com%2Frss.xml'
                   target='blank'><img id='feedlyFollow'
                                       src='https://s3.feedly.com/img/follows/feedly-follow-rectangle-volume-medium_2x.png'
                                       alt='follow us in feedly' width='71' height='28'></a>
            </div>
        </div>
        <div class="sidebar-links">
                <div class="date">Tue Dec 09 2025</div>
                <a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html"> How to Find Flicker Free LED Christmas Lights</a>
                <div class="date">Sat May 13 2023</div>
                <a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html"> Reverse Engineering Netpanzer and Extracting Assets</a>
                <div class="date">Thu Apr 02 2020</div>
                <a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html"> Just How Fast is EJS</a>
                <div class="date">Mon Feb 17 2020</div>
                <a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures For MMO Game Backends in Java</a>
                <div class="date">Fri May 17 2019</div>
                <a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html"> Pixmap Release Launching Rewind</a>
                <div class="date">Sun May 05 2019</div>
                <a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html"> About This</a>
        </div>
    </div>

    <div class="content post-page pure-u-1 pure-u-md-3-4">
        <div>
            <div class="posts">
                <section class="post">
                    <div class="post-description">
                        <p>
                            <h6 id="postdate">Mon Feb 17 2020</h6>
<h1 id="a-hrefhttpsblogwinricklabscom02-17-2020---efficient-data-structures-for-mmo-game-backends-in-javahtmlefficient-data-structures-for-mmo-game-back-ends-in-javaa"><a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures for MMO Game Back-ends in Java</a></h1>
<p><em>Article size is 14.4 kB - 18.6 MB and is a 11 min read</em></p>
<p>... and how I almost rewrote the backend in Rust, but didn&#39;t.</p>
<p>Java gurus beware - none of this is new news. However, it is interesting, and I ran into it recently for the first time dealing with large heaps.
Most Java applications I&#39;ve worked on either had data on the heap very short-lived like per-request or the heap allocation was very temporary (like batch processing).</p>
<h4 id="the-context">The Context</h4>
<p>However, for the past six months I&#39;ve been working on <a href="https://tdworldgame.com">TDWorld</a>. TDWorld can best be described as a global-scale
tower defense MMO. That&#39;s a mouthful, but that&#39;s what it is. It&#39;s a location based game where you claim buildings and connect them. For example,
let&#39;s say you claim your house and then the supermarket down the street. You connect them, and if your creeps make it from your house to the supermarket
you get in-game currency. The game plays on a real map - using real roads and buildings. Your enemies can take a building in between your house and that supermarket,
level it up, and stop your creeps from reaching their destination. Hence, the strategy part.</p>
<h4 id="the-problem">The Problem</h4>
<p>We have a few requirements. We need to be able to simulate vehicle traffic, in the form of in-game creeps, and also support queries that allow towers to
efficiently find the nearby creeps to attack. On top of this we need to sync state with your phone. It&#39;s not a small task.</p>
<p>On top of that, we don&#39;t have a lot of money. I started with MapBox for the traffic routing and quickly racked up thousands of dollars in API requests during
testing. This was a big no-no, so we now run our own instance of the routing server.</p>
<p>For a quick example, here&#39;s a very early version of the client that rendered an android TextureView over MapBox&#39;s SurfaceView by using reflection to grab
its contents and overwrite it (Android devs will know how dirty of a hack this is). But it lets us demonstrate the game. Don&#39;t worry, I&#39;m getting to the technical stuff soon.</p>
<h5 id="interesting-update">Interesting Update</h5>
<pre><code>As of 2022 The game is now built in a custom 3D OSRM renderer on top of libgdx. The article continues with the old 2d renderer.</code></pre><p>This example is playing the game in Redwood City. Ignore the graphics. Just see how the creeps follow the roads and the towers have to shoot them.
Everyone on their phone should see the same thing as you.</p>
<p><img 
    src="https://blog.winricklabs.com/images/map.gif"
    alt="TDWorld Example!"
    title="TDWorld Example!"
    class='lozad' /></p>
<p>This demonstrates the kind of queries we need to support. For this we make heavy use of <a href="https://github.com/davidmoten/rtree" target="_blank">David Moten&#39;s rtree</a>.</p>
<p>That&#39;s not so much what this post is about, I can write a separate post about immutable rtrees. This is about reducing memory, because oh man did we have a problem with that.</p>
<h5 id="interesting-update-1">Interesting Update</h5>
<pre><code>As of 2022, we now use a custom grid-based geospacial index, instead of R-trees which proved to be too complicated and too much overhead.</code></pre><h4 id="technical-juicy-stuff">Technical Juicy Stuff</h4>
<p>Here are all the problems I had, as of last Friday:</p>
<ol>
<li>Seeding was super slow. On launch, we want to have NPCs in every major city in the US (before we launch globally) proportional to the population of said city, randomly distributed from the city&#39;s center. It took almost 30 minutes to seed this data and build the indexes, which was too slow to iterate and develop on.</li>
<li>Saving and restoring game state was super slow. Like a minute to checkpoint to disk and ten minutes to restore.</li>
<li>We were using way too much memory. For around 300k towers and a few thousand creeps we saw heap sizes of 14gb. This could equal hundreds of gigabytes in the real world - needlessly expensive! Also, the connections themselves will take a lot of memory so can&#39;t use all resources just on the game engine.</li>
</ol>
<p>Efficiency is important here because of scale. A poorly coded backend could mean six months of work wasted if we go down on launch day and nobody comes back to the app - and we
don&#39;t have the money or time to scale horizontally. So to &quot;get it out there&quot; we need to have a backend that&#39;ll scale on one machine, up to a million concurrent connections, and handle
tens of millions of actors (creeps) moving around concurrently. For concurrency, we shard in-memory geographically, but I&#39;ll cover this in a separate post. If we need to scale horizontally
it&#39;s just a matter of building an orchestrator to move the shards between instances - and no we can&#39;t back this with a traditional database. Millions of concurrent writes with a high write concern
basically mean that the app layer needs to have its own in-memory database that checkpoints to disk (and that&#39;s what we have). This is also how Redis works!</p>
<p>There was a point where I blamed Java for my poor performance, and I decided a trial rewrite in Rust. I figured I&#39;d just 1:1 port the Java code and see what the performance difference was.</p>
<p>I ported All the structures and then the seeding code. This was a super painful two days, and I am not a Rust fan yet. I think I get Rust, it&#39;s just stressful and not so fun to write.</p>
<p>Anyway, the code written in Rust blew away the code in Java. It could seed the data for a thousand cities, and create all the NPC users, in seconds.</p>
<p>Then I realized I had forgotten to pull out my profiler. N00b mistake. A quick dig into JDK Mission Control showed a number of problems.</p>
<p>I had read the hot path in the Java code a hundred times and couldn&#39;t see why the seeding was so slow. The code was so trivial in fact I just didn&#39;t think of using a profiler.
I suppose I thought it was Java&#39;s fault because I was fighting the garbage collector for a while, but I should have known better because switching to Shenandoah fixed that.</p>
<p>(By the way, Shenandoah is awesome, we went from many second pauses to never seeing more than a few hundred milliseconds STW pauses even during serialization/check pointing to disk)</p>
<p>So anyway. The profiler. Here is the cause of my first problem, the seeding being slow. During seeding, we enqueue a request to route between the source and target towers - and there
can be hundreds of thousands of these requests. The downstream routing server is in C++ but it can still only handle so many requests concurrently, so we queue them up in memory in the
game backend using a custom task executor.</p>
<p>And that executor - uses a ConcurrentLinkedQueue.</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-router.png"
    alt="Router Class Fields"
    title="Router Class Fields"
    class='lozad' /></p>
<p>So what&#39;s wrong with that? Well, every time we enqueue a request we check the count of the queued items to see if we should execute that one right away:</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-router-size.png"
    alt="Router Class Oopsie"
    title="Router Class Oopsie"
    class='lozad' /></p>
<p>The problem is ConcurrentLinkedQueue is implemented using a linked list. We call size() on it A LOT, and each time it was iterating through the whole queue.
I&#39;m not sure why the JDK doesn&#39;t optimize this using atomics, as that would probably introduce less contention than what it&#39;s doing today. All we had to do was use an AtomicInteger to keep track of the count.</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-router-fix.png"
    alt="Router Class Fix"
    title="Router Class Fix"
    class='lozad' /></p>
<p>So that&#39;s #1 solved. The Java version was now faster than Rust.</p>
<p>So now #2</p>
<h3 id="heres-where-we-talk-about-data-structures-sorry">Here&#39;s where we talk about data structures, sorry.</h3>
<p>Ok - I wrote the title of this and then realized it takes a while to actually get to what the title&#39;s about. Thanks for getting this far.</p>
<p>Most of what we need to store in memory is the routing information. Each route - for example between your house and the grocery store - consists of a bunch of steps.</p>
<p>These steps are basically objects with the properties {duration, speed, and lat/lng}. And we store A LOT of these. Whenever a creep is primed to move again, we get the next step and then
update its position in the rtree.</p>
<p>We had originally represented geographical position information using doubles. This gives us a very high degree of accuracy - but at a cost!</p>
<p>Before we optimize, let&#39;s look at some stats. We can optimize forever, so we have to quantify when we&#39;re done. Let&#39;s 5gb of heap is where we&#39;d be happy with our testing dataset.</p>
<p>Note that the creep/tower count is just to some info about one shard that I have running, it doesn&#39;t mean that much.</p>
<h4 id="before-optimization">Before Optimization:</h4>
<p>CREEPS=3934 TOWERS=263278 TICK DURATION=125.0</p>
<p><strong>Size on disk</strong>: 2.322gb</p>
<p><strong>Memory</strong>: 14gb</p>
<h5 id="serialization-checkpoint-and-app-startup-time">Serialization (checkpoint and app startup) Time</h5>
<p><strong>To Disk (nvme)</strong>: 69540ms</p>
<p><strong>From Disk</strong>: 10 minutes</p>
<p>Wow! Over a minute to save! That&#39;s not too bad, however think about the fact that it&#39;s taking that long because it&#39;s <em>doing something</em> intensive. The game engine will already check
for how many threads are free and pause one of the shards if needed to serialize to disk - however we don&#39;t want to do that for a long time.</p>
<p>So first thing&#39;s first. double in the JVM world is 8 bytes. Float is 4. Also, we store a lot of Strings but don&#39;t really make use of the String class. Let&#39;s change everything to float and byte[].</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-floatstring.png"
    alt="GitHub Changes"
    title="GitHub Changes"
    class='lozad' /></p>
<p>Not too bad. Now let&#39;s measure what the affect was.</p>
<h4 id="after-changing-double-to-float-and-string-to-byte">After changing double to float and String to byte[]</h4>
<p>CREEPS=2910 TOWERS=263278 DURATION=202.0</p>
<p><strong>Size on disk</strong>: 1.49gb</p>
<p><strong>Memory</strong>: 12gb</p>
<h5 id="serialization-checkpoint-and-app-startup-time-1">Serialization (checkpoint and app startup) Time</h5>
<p><strong>To Disk</strong>: 64443ms</p>
<p><strong>From Disk</strong>: 9.5 minutes</p>
<p>Alright, we gave up about 10 meters of accuracy (changing geographical coordinate representation) from double to float and got 2gb of ram and minor serialization improvements.</p>
<p>Not great.</p>
<p>However, one thing I realized was that each Route has many Step[] arrays. Each Step just contains a few floats and a Position object now (which also has two floats).</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-route-0.png"
    alt="Route Obj Before Optimization"
    title="Route Obj Before Optimization"
    class='lozad' /></p>
<p>So basically our route information is a series of floats. Each entry in that Step[] array is also eight bytes for the pointer. That <em>really</em> adds up.
Let&#39;s get rid of those objects.</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-route-1.png"
    alt="Route Obj Using float[]"
    title="Route Obj Using float[]"
    class='lozad' /></p>
<p>Then we just need some utilities to access this newly formatted data:</p>
<p><img 
    src="https://blog.winricklabs.com/images/tdw-route-2.png"
    alt="Route Obj Utils"
    title="Route Obj Utils"
    class='lozad' /></p>
<p>So what in the Kotlin is going on here? Well, we&#39;re basically serializing our data at rest into an array. This is much more compact than a bunch of objects and
is a common tactic for databases written in Java - use primitives as much as possible.</p>
<p>This means that this:</p>
<pre class="code">
{
  "route": [
    {
      "duration": 2,
      "speed": 5,
      "position": {
        "lat": 76.4,
        "lng": -170
      }
    },
    {
      "duration": 4,
      "speed": 3,
      "position": {
        "lat": 76.2,
        "lng": -170.1
      }
    }
  ]
}
</pre>

<p>Becomes:</p>
<pre class="code">
{
  "route": [
    2,
    5,
    76.4,
    -170,
    4,
    3,
    76.2,
    -170.1
  ]
}
</pre>

<p>Not only is this much nicer for the garbage collection it&#39;s much better for serialization in general - and not only to disk but over the network.</p>
<p>You&#39;d think that the JVM is optimizing things like this already, but the best it can do is keep the data the references point to
close to each other, and maybe compact the pointers.</p>
<p>The JVM does magical things. But behold.</p>
<p><strong>After redesigning the Route storage</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before Optimizations</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>Size on disk</td>
<td>2.322gb</td>
<td>873mb</td>
</tr>
<tr>
<td>Memory</td>
<td>14gb</td>
<td>1.8gb</td>
</tr>
<tr>
<td>To Disk Time</td>
<td>69540ms</td>
<td>4183ms</td>
</tr>
<tr>
<td>From Disk Time</td>
<td>10 minutes</td>
<td>24 seconds</td>
</tr>
</tbody></table>
<p>Amazing. Our shard size on disk has gone down from over two gigabytes to 873mb. Note that the app is still running in the background and at 527314 creeps the size on disk is still less than a gigabyte.</p>
<p>Our memory usage went from 14gb to less than two.</p>
<p>Startup time went from ten minutes to 24 seconds, and writing to disk went from over a minute to four seconds.</p>
<p>This was a super productive past couple days, and I&#39;m very happy with the result. I&#39;m hoping that soon we can show off the full game and prepare for launch.</p>
<p>Next I&#39;ll be doing a load test with simulating millions of active clients and having the backend continually keep them in sync. This is much harder and will be covered in separate blog posts.
Cheers!</p>

                        </p>
                    </div>
                </section>
                <script src="https://fastcomments.com/js/embed-v2.min.js"></script>
                <div id="fastcomments-widget"></div>
                <script>
                    window.FastCommentsUI(document.getElementById('fastcomments-widget'), {
                        tenantId: 'StrTVey_yMpw'
                    });
                </script>
            </div>
        </div>

        <div class="footer">Copyright winrickLabs LLC 2019 - 2026</div>
    </div>
</div>
</body>
</html>
]]></content:encoded>
            <author>winrid+wlb @ gmail DOT com (Devon Winrick)</author>
        </item>
        <item>
            <title><![CDATA[(5-17-2019) Pixmap Release Launching Rewind]]></title>
            <link>https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html</link>
            <guid>https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html</guid>
            <pubDate>Fri, 17 May 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[(5-17-2019) Pixmap Release Launching Rewind - Size: 3.8 kB - 22 MB. 3 min read]]></description>
            <content:encoded><![CDATA[<html>
<head>
    <title>(5-17-2019) Pixmap Release Launching Rewind | winrickLabs Blog</title>
    <link rel="shortcut icon" href="/images/favicon.png"/>
    <link rel="stylesheet" href="/css/pure-min.css">
    <link rel="stylesheet" href="/css/grids-responsive-min.css">
    <link rel="stylesheet" href="/css/blog.css">
    <script src="js/blog.min.js"></script>
    <script src="https://fastcomments.com/js/comment-ui.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script async src="https://devonsanalytics.xyz/js/watchly.min.js" data-key="5d1fcfabcc00a66e161816b4"></script>
    <link rel="alternate" type="application/rss+xml" title="WinrickLabs Blog" href="https://blog.winricklabs.com/rss.xml">
</head>
<body>
<div id="layout" class="pure-g">
    <div class="sidebar pure-u-1 pure-u-md-1-5">
        <div class="header">
            <h1 class="brand-title"><a href="/">winrickLabs</a></h1>
            <h2 class="brand-tagline">Blog</h2>
            <div class="buttons">
                <a href="https://blog.winricklabs.com/rss.xml"><img src="/images/rss.png"/></a>
                <a href='https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fblog.winricklabs.com%2Frss.xml'
                   target='blank'><img id='feedlyFollow'
                                       src='https://s3.feedly.com/img/follows/feedly-follow-rectangle-volume-medium_2x.png'
                                       alt='follow us in feedly' width='71' height='28'></a>
            </div>
        </div>
        <div class="sidebar-links">
                <div class="date">Tue Dec 09 2025</div>
                <a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html"> How to Find Flicker Free LED Christmas Lights</a>
                <div class="date">Sat May 13 2023</div>
                <a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html"> Reverse Engineering Netpanzer and Extracting Assets</a>
                <div class="date">Thu Apr 02 2020</div>
                <a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html"> Just How Fast is EJS</a>
                <div class="date">Mon Feb 17 2020</div>
                <a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures For MMO Game Backends in Java</a>
                <div class="date">Fri May 17 2019</div>
                <a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html"> Pixmap Release Launching Rewind</a>
                <div class="date">Sun May 05 2019</div>
                <a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html"> About This</a>
        </div>
    </div>

    <div class="content post-page pure-u-1 pure-u-md-3-4">
        <div>
            <div class="posts">
                <section class="post">
                    <div class="post-description">
                        <p>
                            <h6 id="postdate">Fri May 17 2019</h6>
<h1 id="a-hrefhttpsblogwinricklabscom5-17-2019-pixmap-release-launching-rewindhtmlpixmap-release-launching-rewinda"><a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html">Pixmap Release: Launching Rewind</a></h1>
<p><em>Article size is 3.8 kB - 22 MB and is a 3 min read</em></p>
<p>Today I&#39;m excited to announce a new feature for Pixmap. While I had planned on mostly
focusing on the native iOS port in Swift a couple friends brought up an idea that sounded
too fun to implement, so here it is.</p>
<p><img 
    src="https://blog.winricklabs.com/images/pixmap-rewind-demo.gif" 
    alt="Pixmap rewind demo"
    class='lozad'
    height="500px" /></p>
<p>Think version control for pixel art, except it&#39;s just linear. It&#39;s useful and fun to use.
If you buy Pixmap Ultimate you can use rewind to download snapshots or rewind an image.</p>
<p>When you rewind an image it modifies the history like this:</p>
<p><img 
    src="https://blog.winricklabs.com/images/pixmap-how-rewind-works.svg" 
    alt="Pixmap rewind how it works"
    class='lozad'
    height="500px"
    width="80%" /></p>
<p>How it works is pretty simple. Luckily we chose to store every change everyone makes which made this possible.
When you start a rewind the application simply paginates the event history from the backend and when you rewind
an image we use the same architecture for drawing. All the changes get added to the same queue, processed by the same
servers, etc. The only real difference is that a lock is created on the image and then released once the rewind is done
so that no editing can happen during the rewind.</p>
<p>The main challenge client side was performance.</p>
<p>If you drag the bar all the way to the beginning of time and start dragging
it forward that is very &quot;light&quot; since each &quot;tick&quot; of the bar is simply applying changes to the scene from the last tick.
However, when you drag backwards you have to re-render the whole scene from the beginning of time to where the user dragged 
the bar. Doing this on the UI thread resulted in lots of lag, so I had to implement an &quot;observer&quot; thread that waits for
things to render, does the processing, and pushes the result to the UI thread. I also ended up changing how concurrency
worked in the renderer to reduce CPU usage, making both rewind and drawing smoother. </p>
<p>There were three challenges server side.</p>
<p>The first was that since I&#39;m using websockets the original plan was for the client to send an event
and then the server would paginate through the data and send the pages to the client. This didn&#39;t work with websockets
because no matter the frame size or delay between messages the connection could get clogged resulting in high memory server
side, delay in WS handshakes, and ultimately the connection closing. The solution was to implement traditional pagination
over websockets where the client says &quot;give me the next 10k after this id&quot;. At that point you don&#39;t even need websockets
but I already have the infrastructure for it for other features.</p>
<p>For sorting the change list server-side for pagination we follow a &quot;start after this _id&quot; approach Mongo&#39;s
ids are naturally sortable in a non-sharded environment (assuming you let the DB create the id, and you don&#39;t create it application side). This drops
cpu usage from around 80% during a rewind of &quot;Mid Peninsula&quot; to a negligible amount. Also, we can just use the default index for <code>_id</code> instead of
having to create more indexes which would take disk space etc. The only downside is we have to scan some documents before the sort, but at current draw
volume this works fine.</p>
<p>Anyway, I had fun building it and hope people enjoy it. Cheers.</p>

                        </p>
                    </div>
                </section>
                <script src="https://fastcomments.com/js/embed-v2.min.js"></script>
                <div id="fastcomments-widget"></div>
                <script>
                    window.FastCommentsUI(document.getElementById('fastcomments-widget'), {
                        tenantId: 'StrTVey_yMpw'
                    });
                </script>
            </div>
        </div>

        <div class="footer">Copyright winrickLabs LLC 2019 - 2026</div>
    </div>
</div>
</body>
</html>
]]></content:encoded>
            <author>winrid+wlb @ gmail DOT com (Devon Winrick)</author>
        </item>
        <item>
            <title><![CDATA[(5-5-2019) About This]]></title>
            <link>https://blog.winricklabs.com/(5-5-2019)-about-this.html</link>
            <guid>https://blog.winricklabs.com/(5-5-2019)-about-this.html</guid>
            <pubDate>Sun, 05 May 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[(5-5-2019) About This - Size: 333 B. 1 min read]]></description>
            <content:encoded><![CDATA[<html>
<head>
    <title>(5-5-2019) About This | winrickLabs Blog</title>
    <link rel="shortcut icon" href="/images/favicon.png"/>
    <link rel="stylesheet" href="/css/pure-min.css">
    <link rel="stylesheet" href="/css/grids-responsive-min.css">
    <link rel="stylesheet" href="/css/blog.css">
    <script src="js/blog.min.js"></script>
    <script src="https://fastcomments.com/js/comment-ui.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script async src="https://devonsanalytics.xyz/js/watchly.min.js" data-key="5d1fcfabcc00a66e161816b4"></script>
    <link rel="alternate" type="application/rss+xml" title="WinrickLabs Blog" href="https://blog.winricklabs.com/rss.xml">
</head>
<body>
<div id="layout" class="pure-g">
    <div class="sidebar pure-u-1 pure-u-md-1-5">
        <div class="header">
            <h1 class="brand-title"><a href="/">winrickLabs</a></h1>
            <h2 class="brand-tagline">Blog</h2>
            <div class="buttons">
                <a href="https://blog.winricklabs.com/rss.xml"><img src="/images/rss.png"/></a>
                <a href='https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fblog.winricklabs.com%2Frss.xml'
                   target='blank'><img id='feedlyFollow'
                                       src='https://s3.feedly.com/img/follows/feedly-follow-rectangle-volume-medium_2x.png'
                                       alt='follow us in feedly' width='71' height='28'></a>
            </div>
        </div>
        <div class="sidebar-links">
                <div class="date">Tue Dec 09 2025</div>
                <a href="https://blog.winricklabs.com/(12-9-2025)-how-to-find-flicker-free-led-christmas-lights.html"> How to Find Flicker Free LED Christmas Lights</a>
                <div class="date">Sat May 13 2023</div>
                <a href="https://blog.winricklabs.com/(5-13-2023)-reverse-engineering-netpanzer-and-extracting-assets.html"> Reverse Engineering Netpanzer and Extracting Assets</a>
                <div class="date">Thu Apr 02 2020</div>
                <a href="https://blog.winricklabs.com/(04-02-2020)-just-how-fast-is-ejs.html"> Just How Fast is EJS</a>
                <div class="date">Mon Feb 17 2020</div>
                <a href="https://blog.winricklabs.com/(02-17-2020)---efficient-data-structures-for-mmo-game-backends-in-java.html">Efficient Data Structures For MMO Game Backends in Java</a>
                <div class="date">Fri May 17 2019</div>
                <a href="https://blog.winricklabs.com/(5-17-2019)-pixmap-release-launching-rewind.html"> Pixmap Release Launching Rewind</a>
                <div class="date">Sun May 05 2019</div>
                <a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html"> About This</a>
        </div>
    </div>

    <div class="content post-page pure-u-1 pure-u-md-3-4">
        <div>
            <div class="posts">
                <section class="post">
                    <div class="post-description">
                        <p>
                            <h6 id="postdate">Sun May 05 2019</h6>
<h1 id="a-hrefhttpsblogwinricklabscom5-5-2019-about-thishtmlwhat-is-thisa"><a href="https://blog.winricklabs.com/(5-5-2019)-about-this.html">What is this?</a></h1>
<p><em>Article size is 333 B and is a 1 min read</em></p>
<p>This is a small technical blog for fun projects or explorations.</p>

                        </p>
                    </div>
                </section>
                <script src="https://fastcomments.com/js/embed-v2.min.js"></script>
                <div id="fastcomments-widget"></div>
                <script>
                    window.FastCommentsUI(document.getElementById('fastcomments-widget'), {
                        tenantId: 'StrTVey_yMpw'
                    });
                </script>
            </div>
        </div>

        <div class="footer">Copyright winrickLabs LLC 2019 - 2026</div>
    </div>
</div>
</body>
</html>
]]></content:encoded>
            <author>winrid+wlb @ gmail DOT com (Devon Winrick)</author>
        </item>
    </channel>
</rss>