<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:fh="http://purl.org/syndication/history/1.0"><channel><title>Yulin AWS Simulator | Development blog</title><description>Documentation for Yulin AWS simulator for tests, local development, and CI.</description><link>https://yulinsim.dev/</link><language>en</language><fh:complete/><atom:link rel="self" href="https://yulinsim.dev/blog/rss.xml"/><item><title>Efficiently advancing simulated AWS time without real timers</title><link>https://yulinsim.dev/blog/simulated-time-without-real-timers/</link><guid isPermaLink="true">https://yulinsim.dev/blog/simulated-time-without-real-timers/</guid><description>Advancing a simulation&apos;s clock by 30 days took 17 seconds of real time, because the simulator was waiting for unnecessary Node.js timers.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Yulin has a &lt;a href=&quot;https://yulinsim.dev/time/&quot; title=&quot;Simulated AWS clock time&quot;&gt;simulated clock&lt;/a&gt; feature that lets you freeze,
resume and advance time in each simulated AWS instance. Advancing the sim clock triggers any
scheduled tasks
like &lt;a href=&quot;https://yulinsim.dev/services/scheduler/&quot; title=&quot;Simulated EventBridge Scheduler&quot;&gt;EventBridge schedules&lt;/a&gt; that would have
occurred in that interval before returning.&lt;/p&gt;
&lt;p&gt;The first implementation of this turned out to have a performance bug which caused it to take around
17 seconds to advance the sim clock by 30 days. That might still sound like quite a good ratio, but
it should take milliseconds. One of the main features of Yulin is the ability to test significant
system behaviours in a small amount of real time.&lt;/p&gt;
&lt;p&gt;A relatively short example showing the kind of situation where the performance problem would arise:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { PutMetricAlarmCommand } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@aws-sdk/client-cloudwatch&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { SimAws } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@kensio/yulin&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;simAws&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SimAws&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;metrics&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;simAws&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cloudWatch&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;AlarmName&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;OrdersFailing&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PaymentsFailing&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; metrics&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;putMetricAlarm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PutMetricAlarmCommand&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;AlarmName,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Namespace: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Orders&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MetricName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Failed&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Statistic: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sum&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Period: &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;EvaluationPeriods: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;DatapointsToAlarm: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Threshold: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ComparisonOperator: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;GreaterThanThreshold&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; simAws&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clock&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;advanceBy&lt;/span&gt;&lt;span&gt;({ days: &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Advancing the sim clock does not skip straight to the end. It runs any events and tasks that would
have occurred in that interval before the &lt;code dir=&quot;auto&quot;&gt;advanceBy()&lt;/code&gt; call returns. This all happens inside the
same single Node.js process, so you can do things like collecting test coverage and stepping through
the system in a debugger.&lt;/p&gt;
&lt;p&gt;Because of that need, any work scheduled between the starting sim time and the end of the interval
has to be triggered. The implementation steps through each instant at which something is due to
occur. Yulin currently evaluates each sim CloudWatch alarm at its configured period boundary.&lt;/p&gt;
&lt;p&gt;Thirty days of five-minute intervals is 8,640, and with the
two &lt;a href=&quot;https://yulinsim.dev/services/cloudwatch/&quot; title=&quot;Simulated CloudWatch metrics &amp;#x26; alarms&quot;&gt;sim CloudWatch alarms&lt;/a&gt;
configured in the example above, that doubles to 17,280.&lt;/p&gt;
&lt;p&gt;Computers are quite fast, so 17,280 in itself is not a large number for a computer to cope with. The
performance problem arose because the first implementation of this used
a &lt;a href=&quot;https://nodejs.org/api/timers.html&quot;&gt;Node.js timer&lt;/a&gt; to defer background tasks during the interval.&lt;/p&gt;
&lt;p&gt;The scheduler passed a zero-millisecond delay, equivalent to &lt;code dir=&quot;auto&quot;&gt;setTimeout (..., 0)&lt;/code&gt;. Node clamps any
timeout below one millisecond to one millisecond, but also does not guarantee that it will run at
exactly the scheduled time. That meant that every trip through that path introduced a real timer
with a minimum requested delay of one millisecond.&lt;/p&gt;
&lt;p&gt;Those accumulated one-millisecond waits were unnecessary anyway. The first implementation of the
&lt;code dir=&quot;auto&quot;&gt;advanceBy()&lt;/code&gt; loop already waited for each task to finish before it picked up the next one, so
deferring tasks with a timer was just shuffling work around on the loop without fundamentally
changing anything.&lt;/p&gt;
&lt;p&gt;The fix was pretty small:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private async &lt;/span&gt;&lt;span&gt;runDueTasksUpTo&lt;/span&gt;&lt;span&gt;(instant: Date): &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;let due &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;background&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;takeNextDueBy&lt;/span&gt;&lt;span&gt;(instant);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;due&lt;/span&gt;&lt;span&gt; !== &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clock&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;laterOfNow&lt;/span&gt;&lt;span&gt;(due&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dueTime&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;background&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;schedule&lt;/span&gt;&lt;span&gt;(due&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; due&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;task&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;background&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;complete&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;due &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;background&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;takeNextDueBy&lt;/span&gt;&lt;span&gt;(instant);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The performance fix was to replace &lt;code dir=&quot;auto&quot;&gt;this.background.schedule(due.task);&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;await due.task();&lt;/code&gt;.
That avoided the problem with minimum one-millisecond waits on &lt;code dir=&quot;auto&quot;&gt;setTimeout(0)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After that change, advancing the sim clock by 30 days went from around 17 seconds down to around 24
milliseconds, more than a 700-fold improvement.&lt;/p&gt;
&lt;p&gt;The main takeaway from this is that Yulin has to keep simulated AWS time separate from real time in
the host process. For behaviour driven by simulated AWS time, the host clock should not accidentally
introduce real waits. Separating simulation time from real time is what allows for efficient
lightweight testing and flexible local development.&lt;/p&gt;
</content:encoded></item></channel></rss>