Universal Dashboard is a web framework for PowerShell. It allows you to create websites and REST APIs with just PowerShell script. Unlike other managed languages, like C# and F#, PowerShell is not compiled completely to highly optimized IL code during a compilation step. Instead, it is parsed, tokenized, interpreted and compiled during runtime. This results in a huge difference in the performance of other IL-based languages. In a framework such as Universal Dashboard, this can be especially evident.

In this blog post, we will look at some performance considerations when looking at using Universal Dashboard as your platform for your next web project.

Base Line

The baseline performance for the Universal Dashboard web-server is below. Universal Dashboard is built on ASP.NET Core. Although ASP.NET Core is capable of extremely high requests per second, Universal Dashboard clocks in a bit lower. The reason for this is that each request must allocate a runspace from a runspace pool, set up the runspace for execution, parse and execute a PowerShell Script Block and then serialize any results to JSON.

You can see below that the web server was capable of about 1000 requests per second on a machine with 8 CPU cores. Your results may vary but this should suffice for most low-to-medium traffic internal tools.

PS C:\Users\adamr> Measure-Command { 1..8 | % { Start-ThreadJob { 1..1000 | % { Invoke-WebRequest http://localhost:10004/test  } }  } | Wait-Job }
 
 Days              : 0
 Hours             : 0
 Minutes           : 0
 Seconds           : 8
 Milliseconds      : 432

Performance Tips

Cache Whenever Possible

Most of the time, the performance issues people face with Universal Dashboard have nothing to do with Universal Dashboard. Running PowerShell scripts can be slow. The is especially true when accessing remote resources. Users expect quick response from websites. To aid in this, it’s suggested to utilize Scheduled Endpoints and the Cache scope to avoid having to load resources every time a page loads.

For example, assume that I’m calling a remote REST API to load some resources in an endpoint that then displays them in a grid.

New-UDGrid -Title 'Movies' -Endpoint {
     Invoke-RestMethod http://movieindex.io/api/movies | Out-UDGridData
}

Due to the nature of how Endpoint script blocks work, the above would call the movieindex.io REST API every time the page is loaded. If you have many users accessing your dashboard, this means that you will have many calls to movieindex.io.

In order to improve the performance of this type of component, you can instead load the movie data on an interval and show users cached data. Instead of each user reading directly from the movieindex.io API, they are now reading from the Universal Dashboard Cache scope’s memory.

$Schedule = New-UDEndpointSchedule -Every 10 -Minute
 $Endpoint = New-UDEndpoint -Schedule $Schedule -Endpoint {
     $Cache:Movies = Invoke-RestMethod http://movieindex.io/api/movies
 }
 New-UDGrid -Title 'Movies' -Endpoint {
     $Cache:Movies | Out-UDGridData
 }

You can cache any amount of data you’d like up until you run out of memory on your machine. Be careful with caching large database tables in memory.

Favor Content over Endpoint

Content and Endpoint script blocks can be confusing. The main difference between a Content and an Endpoint script block is that Content is executed at the time the cmdlet is run and an Endpoint script block is executed when a component is loaded on the page.

For example, if we have a New-UDElement with the content below, the script block itself is actually executed when the New-UDElement is called.

New-UDElement -Tag 'div' -Content {
     New-UDElement -Tag 'div' -Content {'I run right away!'}
}

Alternatively, if you use an Endpoint, something different happens. The Endpoint script block is not run when the cmdlet is executed. Instead, it is cached inside the Universal Dashboard Endpoint Service for execution at a later time. Typically, this later time is when the component is loaded on the page.

New-UDElement -Tag 'div' -Endpoint {
     New-UDElement -Tag 'div' -Content {'I run when the page is loaded!'}
}

There is a visible performance difference between these different methods. The first method returns all the data during the first HTTP request from the server. The second method requires a second HTTP method to call back to the server to execute the endpoint script block and return the resulting data.

This can be especially tricky when nesting many Endpoint script blocks together. The below example requires 5 HTTP requests to be made.

New-UDElement -Tag 'div' -Endpoint {
     New-UDElement -Tag 'div' -Endpoint {'I run when the page is loaded!'}
     New-UDElement -Tag 'div' -Endpoint {'I run when the page is loaded!'}
     New-UDElement -Tag 'div' -Endpoint {'I run when the page is loaded!'}
}

The benefit of the Endpoint script block is that it allows for dynamic data and controls to be generated when the page is loaded. This is a huge feature of UD. It’s recommended to wrap the outer most component, where it makes sense, in an endpoint and generate the inner components with the Content script block.

The below example creates an outer most div using the Endpoint script block. This means it will require an HTTP request to load the data for the content of component. The inner components use content so they will not require another HTTP request back to the server. They are still dynamic because they are nested within a dynamic Endpoint script block.

New-UDElement -Tag 'div' -Endpoint {
     $DateTime = Get-Date
     New-UDElement -Tag 'div' -Content {$DateTime.Hour}
     New-UDElement -Tag 'div' -Content {$DateTime.Minute}
     New-UDElement -Tag 'div' -Content {$DateTime.Second}
}

Avoid Overuse of New-UDElement

New-UDElement is a very versatile component that allows you to create any HTML element, hook up event handlers and set attributes. The downside with New-UDElement is it requires a lot of information to be sent from the server to the web browser. With each New-UDElement call, the tag, attributes, and any event handlers need to be communicated to the client machine.

Using purpose-built controls, such as New-UDChart, require only the data to be sent to the client rather than all the HTML information.

The previous version of Universal Dashboard used New-UDElement heavily for many of the standard components. Rather than defining JavaScript components and then sending data via purpose-built cmdlets, cmdlets such as New-UDButton, New-UDFab, and New-UDCollapsible, defined their entire structure and data using New-UDElement. This resulted in the JSON payload sent from the UD server to the browser to be very large.

For example, in Universal Dashboard 2.2.0, creating a basic UDCollapsible with a single collapsible item was 1203 characters.

PS C:\Users\adamr> (New-UDCollapsible -Items { New-UDCollapsibleItem -Title "Test" -Content { } } | ConvertTo-Json -Compress).Length
1203

In Univeral Dashboard 2.4.1, the same command returns a JSON payload of 397 characters.

PS C:\Users\adamr> (New-UDCollapsible -Items { New-UDCollapsibleItem -Title "Test" -Content { } } | ConvertTo-Json -Compress).Length
397

Due to this reason, many of the most common controls have now been built into React components. If you want to build your own React components for Universal Dashboard, check out this repository.