├── .gitattributes ├── .github └── workflows │ ├── azure-static-web-apps-agreeable-mud-0b27ba210.yml │ └── github-pages.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── PrimeView.sln ├── README.md └── src ├── .editorconfig ├── Entities ├── CPUInfo.cs ├── DockerInfo.cs ├── Entities.csproj ├── IReportReader.cs ├── OperatingSystemInfo.cs ├── README.md ├── Report.cs ├── ReportSummary.cs ├── Result.cs ├── Runner.cs └── SystemInfo.cs ├── Frontend ├── App.razor ├── Filters │ ├── FilterExtensions.cs │ ├── IResultFilterPropertyProvider.cs │ ├── LeaderboardFilterPreset.cs │ ├── MultithreadedLeaderboardFilterPreset.cs │ └── ResultFilterPreset.cs ├── Frontend.csproj ├── Pages │ ├── Index.razor │ ├── Index.razor.cs │ ├── ReportDetails.razor │ └── ReportDetails.razor.cs ├── Parameters │ ├── PropertyParameterMap.cs │ ├── QueryStringParameterAttribute.cs │ └── QueryStringParameterExtensions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── README.md ├── ReportExporters │ ├── ExcelConverter.cs │ └── JsonConverter.cs ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── SystemInformation.razor │ ├── ValueTableRow.razor │ └── ValueTableRow.razor.css ├── Sorting │ ├── SortedTablePage.razor │ ├── SortedTablePage.razor.cs │ └── SortingExtensions.cs ├── Tools │ ├── Constants.cs │ ├── ILanguageInfoProvider.cs │ └── LanguageInfo.cs ├── _Imports.razor └── wwwroot │ ├── appsettings.json │ ├── css │ └── app.css │ ├── data │ ├── langmap.json │ ├── report1.json │ ├── report2.json │ └── report3.json │ ├── favicon.ico │ ├── img │ └── logo.svg │ ├── index.html │ ├── js │ └── app.js │ └── staticwebapp.config.json ├── JsonFileReader ├── Constants.cs ├── ExtensionMethods.cs ├── JsonFileReader.csproj ├── README.md ├── ReportReader.cs └── S3BucketIndexReader.cs └── RestAPIReader ├── Constants.cs ├── ExtensionMethods.cs ├── OpenAPIs └── v1.json ├── README.md ├── ReportReader.cs └── RestAPIReader.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-agreeable-mud-0b27ba210.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - staging 8 | pull_request: 9 | types: [opened, synchronize, reopened, closed] 10 | branches: 11 | - staging 12 | - main 13 | 14 | jobs: 15 | build_and_deploy_job: 16 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 17 | runs-on: ubuntu-latest 18 | name: Build and Deploy Job 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: true 23 | - name: Build And Deploy 24 | id: builddeploy 25 | uses: Azure/static-web-apps-deploy@v1 26 | with: 27 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AGREEABLE_MUD_0B27BA210 }} 28 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 29 | action: "upload" 30 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 31 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 32 | app_location: "src/Frontend" # App source code path 33 | api_location: "" # Api source code path - optional 34 | output_location: "wwwroot" # Built app content directory - optional 35 | skip_deploy_on_missing_secrets: true 36 | build_timeout_in_minutes: 30 37 | ###### End of Repository/Build Configurations ###### 38 | 39 | close_pull_request_job: 40 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 41 | runs-on: ubuntu-latest 42 | name: Close Pull Request Job 43 | steps: 44 | - name: Close Pull Request 45 | id: closepullrequest 46 | uses: Azure/static-web-apps-deploy@v1 47 | with: 48 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AGREEABLE_MUD_0B27BA210 }} 49 | skip_deploy_on_missing_secrets: true 50 | action: "close" 51 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy-to-github-pages: 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: 8.0.x 20 | 21 | - name: Install wasm-tools for AOT compilation 22 | run: dotnet workload install wasm-tools 23 | 24 | - name: Publish Frontend project 25 | run: dotnet publish src/Frontend/Frontend.csproj -c Release -o release --nologo 26 | 27 | - name: Change base-tag in index.html from / to PrimeView 28 | run: sed -i 's///g' release/wwwroot/index.html 29 | 30 | - name: Copy index.html to 404.html 31 | run: cp release/wwwroot/index.html release/wwwroot/404.html 32 | 33 | - name: Remove Azure static web app config 34 | run: rm release/wwwroot/staticwebapp.* 35 | 36 | - name: Add .nojekyll file 37 | run: touch release/wwwroot/.nojekyll 38 | 39 | - name: Commit wwwroot to GitHub Pages 40 | uses: JamesIves/github-pages-deploy-action@v4 41 | with: 42 | branch: pages 43 | folder: release/wwwroot 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ 335 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch and Debug Standalone Blazor WebAssembly App", 6 | "type": "blazorwasm", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}/src/Frontend" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Frontend/Frontend.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Frontend/Frontend.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/Frontend/Frontend.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /PrimeView.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frontend", "src\Frontend\Frontend.csproj", "{2C7A4DC8-FAA6-4EAF-BBA5-164E270413CA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Entities", "src\Entities\Entities.csproj", "{EE9F9309-F849-4592-A0D0-10A6D97B7B88}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonFileReader", "src\JsonFileReader\JsonFileReader.csproj", "{50274BDF-B240-499F-B630-BDA25A524EBC}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C370EA12-D676-4F60-8E13-D86837301F84}" 13 | ProjectSection(SolutionItems) = preProject 14 | src\.editorconfig = src\.editorconfig 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestAPIReader", "src\RestAPIReader\RestAPIReader.csproj", "{7EF46961-0D84-478F-B887-182AA02ECE59}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {2C7A4DC8-FAA6-4EAF-BBA5-164E270413CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {2C7A4DC8-FAA6-4EAF-BBA5-164E270413CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {2C7A4DC8-FAA6-4EAF-BBA5-164E270413CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {2C7A4DC8-FAA6-4EAF-BBA5-164E270413CA}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {EE9F9309-F849-4592-A0D0-10A6D97B7B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {EE9F9309-F849-4592-A0D0-10A6D97B7B88}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {EE9F9309-F849-4592-A0D0-10A6D97B7B88}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {EE9F9309-F849-4592-A0D0-10A6D97B7B88}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {50274BDF-B240-499F-B630-BDA25A524EBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {50274BDF-B240-499F-B630-BDA25A524EBC}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {50274BDF-B240-499F-B630-BDA25A524EBC}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {50274BDF-B240-499F-B630-BDA25A524EBC}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {7EF46961-0D84-478F-B887-182AA02ECE59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {7EF46961-0D84-478F-B887-182AA02ECE59}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {7EF46961-0D84-478F-B887-182AA02ECE59}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {7EF46961-0D84-478F-B887-182AA02ECE59}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {68881999-E5C2-4B7E-8855-393E905656CA} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PrimeView 2 | 3 | ![CI](https://github.com/rbergen/PrimeView/actions/workflows/azure-static-web-apps-agreeable-mud-0b27ba210.yml/badge.svg) 4 | ![CI](https://github.com/PlummersSoftwareLLC/PrimeView/actions/workflows/github-pages.yml/badge.svg) 5 | 6 | This is a Blazor WebAssembly static in-browser web application to view benchmark reports generated in/for the [Primes](https://github.com/PlummersSoftwareLLC/Primes) project. 7 | 8 | The application loads benchmark reports from an API that has been developed and published for the purpose. More information about the API reader can be found in [RestAPIReader/README.md](src/RestAPIReader/README.md). 9 | 10 | Previously, the application loaded benchmark reports in JSON format, either from a configured location or using a default approach. The report reader in question is still included; more information on how it works can be found in [JsonFileReader/README.md](src/JsonFileReader/README.md). 11 | 12 | As the report reader back-ends are isolated from the front-end (and added via dependency injection), it's easy to add and use another report provider. 13 | 14 | ## Building 15 | 16 | The solution can be built by running the following commands from the repository root directory, once [.NET 8.0](https://dotnet.microsoft.com/download/dotnet/8.0) is installed: 17 | 18 | ```shell 19 | dotnet workload install wasm-tools-net6 20 | dotnet publish 21 | ``` 22 | 23 | Note that: 24 | 25 | * the first command installs the tools required for AOT compilation. That command has to be executed only once for any system PrimeView is built on. 26 | * the AOT compilation can take multiple minutes to complete. 27 | 28 | At the end of the build process, the location of the build output will be indicated in the following line: 29 | 30 | ```shell 31 | Frontend -> \src\Frontend\bin\Release\net8.0\publish\ 32 | ``` 33 | 34 | ## Implementation notes 35 | 36 | Where applicable, implementation notes can be found in README.md files in the directories for the respective C#/Blazor projects. 37 | 38 | ## Attribution 39 | 40 | * The source code that gets and sets query string parameters is based on [a blog post](https://www.meziantou.net/bind-parameters-from-the-query-string-in-blazor.htm) by Gérald Barré. 41 | * Local storage is implemented using [Blazored LocalStorage](https://github.com/Blazored/LocalStorage). 42 | * The tables of report summaries and report results are implemented using [BlazorTable](https://github.com/IvanJosipovic/BlazorTable). 43 | * The checkered flag in favicon.ico was made by [Freepik](https://www.freepik.com) from [www.flaticon.com](https://www.flaticon.com/). 44 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | # IDE0003: Remove qualification 4 | dotnet_style_qualification_for_field =false:suggestion 5 | 6 | # IDE0003: Remove qualification 7 | dotnet_diagnostic.IDE0003.severity = none 8 | -------------------------------------------------------------------------------- /src/Entities/CPUInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace PrimeView.Entities 6 | { 7 | public class CPUInfo 8 | { 9 | public string? Manufacturer { get; set; } 10 | public string? Brand { get; set; } 11 | public string? Vendor { get; set; } 12 | public string? Family { get; set; } 13 | public string? Model { get; set; } 14 | public string? Stepping { get; set; } 15 | public string? Revision { get; set; } 16 | public string? Voltage { get; set; } 17 | public float? Speed { get; set; } 18 | [JsonPropertyName("speedMin")] 19 | public float? MinimumSpeed { get; set; } 20 | [JsonPropertyName("speedMax")] 21 | public float? MaximumSpeed { get; set; } 22 | public string? Governor { get; set; } 23 | public int? Cores { get; set; } 24 | public int? PhysicalCores { get; set; } 25 | public int? EfficiencyCores { get; set; } 26 | public int? PerformanceCores { get; set; } 27 | public int? Processors { get; set; } 28 | public string? RaspberryProcessor { get; set; } 29 | public string? Socket { get; set; } 30 | public string? Flags { get; set; } 31 | [JsonIgnore] 32 | public string[]? FlagValues => Flags?.Split(' ', StringSplitOptions.RemoveEmptyEntries); 33 | public bool? Virtualization { get; set; } 34 | public Dictionary? Cache { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Entities/DockerInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public class DockerInfo 6 | { 7 | public string? KernelVersion { get; set; } 8 | public string? OperatingSystem { get; set; } 9 | public string? OSVersion { get; set; } 10 | public string? OSType { get; set; } 11 | public string? Architecture { get; set; } 12 | [JsonPropertyName("ncpu")] 13 | public int? CPUCount { get; set; } 14 | [JsonPropertyName("memTotal")] 15 | public long? TotalMemory { get; set; } 16 | public string? ServerVersion { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Entities/Entities.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | PrimeView.Entities 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Entities/IReportReader.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public interface IReportReader 6 | { 7 | Task<(ReportSummary[] summaries, int total)> GetSummaries(int maxSummaryCount); 8 | Task<(ReportSummary[] summaries, int total)> GetSummaries(string? runnerId, int skipFirst, int maxSummaryCount); 9 | Task GetReport(string id); 10 | Task GetRunners(); 11 | void FlushCache(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Entities/OperatingSystemInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public class OperatingSystemInfo 6 | { 7 | public string? Platform { get; set; } 8 | [JsonPropertyName("distro")] 9 | public string? Distribution { get; set; } 10 | public string? Release { get; set; } 11 | public string? CodeName { get; set; } 12 | public string? Kernel { get; set; } 13 | [JsonPropertyName("arch")] 14 | public string? Architecture { get; set; } 15 | public string? CodePage { get; set; } 16 | public string? LogoFile { get; set; } 17 | public string? Build { get; set; } 18 | public string? ServicePack { get; set; } 19 | [JsonPropertyName("uefi")] 20 | public bool? IsUefi { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Entities/README.md: -------------------------------------------------------------------------------- 1 | # Implementation notes 2 | 3 | * The [`IReportReader`](IReportReader.cs) interface defines the "contract" between the web front-end (as implemented in the [Frontend](../Frontend) project), and the back-ends that collect the actual data to show (as currently implemented by the [JsonFileReader](../JsonFileReader) project). 4 | * The root classes used within `IReportReader` are [`ReportSummary`](ReportSummary.cs) and [`Report`](Report.cs) for the report overview and report detail pages, respectively. All other classes are embedded within `Report` instances. -------------------------------------------------------------------------------- /src/Entities/Report.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public class Report 6 | { 7 | public string? Id { get; set; } 8 | public string? User { get; set; } 9 | public DateTime? Date { get; set; } 10 | public CPUInfo? CPU { get; set; } 11 | public OperatingSystemInfo? OperatingSystem { get; set; } 12 | public SystemInfo? System { get; set; } 13 | public DockerInfo? DockerInfo { get; set; } 14 | public Result[]? Results { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Entities/ReportSummary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public class ReportSummary 6 | { 7 | public string? Id { get; set; } 8 | public DateTime? Date { get; set; } 9 | public string? User { get; set; } 10 | public string? CpuVendor { get; set; } 11 | public string? CpuBrand { get; set; } 12 | public int? CpuCores { get; set; } 13 | public int? CpuProcessors { get; set; } 14 | public string? OsPlatform { get; set; } 15 | public string? OsDistro { get; set; } 16 | public string? OsRelease { get; set; } 17 | public string? Architecture { get; set; } 18 | public bool? IsSystemVirtual { get; set; } 19 | public string? DockerArchitecture { get; set; } 20 | public int ResultCount { get; set; } = 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Entities/Result.cs: -------------------------------------------------------------------------------- 1 | using OfficeOpenXml.Attributes; 2 | using OfficeOpenXml.Table; 3 | 4 | namespace PrimeView.Entities 5 | { 6 | [EpplusTable(PrintHeaders = true, ShowTotal = true, TableStyle = TableStyles.Light1)] 7 | public class Result 8 | { 9 | public const int LanguageColumnIndex = 1; 10 | [EpplusTableColumn(Order = LanguageColumnIndex, TotalsRowLabel = "Count:")] 11 | public string? Language { get; set; } 12 | 13 | public const int SolutionColumnIndex = 2; 14 | [EpplusTableColumn(Order = SolutionColumnIndex, TotalsRowFunction = RowFunctions.Count)] 15 | public string? Solution { get; set; } 16 | 17 | public const int SolutionUriColumnIndex = 3; 18 | [EpplusTableColumn(Order = SolutionUriColumnIndex, Header = "Solution link")] 19 | public string? SolutionUrl { get; set; } 20 | 21 | public const int LabelColumnIndex = 4; 22 | [EpplusTableColumn(Order = LabelColumnIndex)] 23 | public string? Label { get; set; } 24 | 25 | public const int IsMultiThreadedColumnIndex = 5; 26 | [EpplusTableColumn(Order = IsMultiThreadedColumnIndex, Header = "Multithreaded?")] 27 | public bool IsMultiThreaded => Threads > 1; 28 | 29 | public const int PassesColumnIndex = 6; 30 | [EpplusTableColumn(Order = PassesColumnIndex, Header = "Number of passes")] 31 | public long? Passes { get; set; } 32 | 33 | public const int DurationColumnIndex = 7; 34 | [EpplusTableColumn(Order = DurationColumnIndex)] 35 | public double? Duration { get; set; } 36 | 37 | public const int ThreadsColumnIndex = 8; 38 | [EpplusTableColumn(Order = ThreadsColumnIndex, Header = "Number of threads")] 39 | public int? Threads { get; set; } 40 | 41 | public const int PassesPerSecondColumnIndex = 9; 42 | [EpplusTableColumn(Order = PassesPerSecondColumnIndex, Header = "Passes / thread / second")] 43 | public double? PassesPerSecond => (double?)Passes / Threads / Duration; 44 | 45 | public const int AlgorithmColumnIndex = 10; 46 | [EpplusTableColumn(Order = AlgorithmColumnIndex)] 47 | public string? Algorithm { get; set; } 48 | 49 | public const int IsFaithfulColumnIndex = 11; 50 | [EpplusTableColumn(Order = IsFaithfulColumnIndex, Header = "Faithful?")] 51 | public bool? IsFaithful { get; set; } 52 | 53 | public const int BitsColumnIndex = 12; 54 | [EpplusTableColumn(Order = BitsColumnIndex, Header = "Bits per prime")] 55 | public int? Bits { get; set; } 56 | 57 | [EpplusIgnore] 58 | public string? Status { get; set; } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Entities/Runner.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public class Runner 6 | { 7 | public string? Id { get; set; } 8 | public string? User { get; set; } 9 | public CPUInfo? CPU { get; set; } 10 | public OperatingSystemInfo? OperatingSystem { get; set; } 11 | public SystemInfo? System { get; set; } 12 | public DockerInfo? DockerInfo { get; set; } 13 | 14 | public string Description 15 | { 16 | get 17 | { 18 | StringBuilder builder = new(); 19 | 20 | if (User != null) 21 | builder.Append($"{User}'s "); 22 | 23 | if (OperatingSystem?.Architecture != null) 24 | builder.Append($"{OperatingSystem.Architecture} "); 25 | 26 | if (CPU?.Vendor != null) 27 | builder.Append($"{CPU.Vendor} "); 28 | 29 | if (CPU?.Brand != null) 30 | builder.Append($"{CPU.Brand} "); 31 | 32 | if (CPU?.Cores != null) 33 | builder.Append($"({CPU.Cores} cores) "); 34 | 35 | if (OperatingSystem?.Distribution != null || OperatingSystem?.Release != null) 36 | builder.Append($"running "); 37 | 38 | if (OperatingSystem?.Distribution != null) 39 | builder.Append($"{OperatingSystem.Distribution} "); 40 | 41 | if (OperatingSystem?.Release != null) 42 | builder.Append($"{OperatingSystem.Release}"); 43 | 44 | return builder.ToString().TrimEnd(); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Entities/SystemInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PrimeView.Entities 4 | { 5 | public class SystemInfo 6 | { 7 | public string? Manufacturer { get; set; } 8 | public string? Model { get; set; } 9 | public string? Version { get; set; } 10 | public string? SKU { get; set; } 11 | public string? RaspberryManufacturer { get; set; } 12 | public string? RaspberryType { get; set; } 13 | public string? RaspberryRevision { get; set; } 14 | [JsonPropertyName("virtual")] 15 | public bool? IsVirtual { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Frontend/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/Frontend/Filters/FilterExtensions.cs: -------------------------------------------------------------------------------- 1 | using PrimeView.Entities; 2 | using PrimeView.Frontend.Pages; 3 | using PrimeView.Frontend.Tools; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | 9 | namespace PrimeView.Frontend.Filters 10 | { 11 | public static class FilterExtensions 12 | { 13 | private static readonly Func successfulResult; 14 | 15 | static FilterExtensions() 16 | { 17 | Expression> successfulResultExpression = result => result.Status == null || result.Status == "success"; 18 | successfulResult = successfulResultExpression.Compile(); 19 | } 20 | 21 | public static IEnumerable Viewable(this IEnumerable source) 22 | { 23 | return source.Where(successfulResult); 24 | } 25 | 26 | public static IEnumerable ApplyFilters(this IEnumerable source, ReportDetails page) 27 | { 28 | var filterLanguages = page.FilterLanguages; 29 | 30 | if (filterLanguages.Count > 0) 31 | source = source.Where(r => filterLanguages.Contains(r.Language)); 32 | 33 | var filteredResults = source.Where(r => 34 | r.IsMultiThreaded switch 35 | { 36 | true => page.FilterParallelMultithreaded, 37 | _ => page.FilterParallelSinglethreaded 38 | } 39 | && r.Algorithm switch 40 | { 41 | "base" => page.FilterAlgorithmBase, 42 | "wheel" => page.FilterAlgorithmWheel, 43 | _ => page.FilterAlgorithmOther 44 | } 45 | && r.IsFaithful switch 46 | { 47 | true => page.FilterFaithful, 48 | _ => page.FilterUnfaithful 49 | } 50 | && r.Bits switch 51 | { 52 | null => page.FilterBitsUnknown, 53 | 1 => page.FilterBitsOne, 54 | _ => page.FilterBitsOther 55 | } 56 | ); 57 | 58 | return page.OnlyHighestPassesPerSecondPerThreadPerLanguage 59 | ? filteredResults 60 | .GroupBy(r => r.Language) 61 | .SelectMany(group => group.Where(r => r.PassesPerSecond == group.Max(r => r.PassesPerSecond))) 62 | : filteredResults; 63 | } 64 | 65 | public static string CreateSummary(this IResultFilterPropertyProvider filter, ILanguageInfoProvider languageInfoProvider) 66 | { 67 | List segments = 68 | [ 69 | filter.FilterLanguages.Count switch 70 | { 71 | 0 => "all languages", 72 | 1 => $"{languageInfoProvider.GetLanguageInfo(filter.FilterLanguages[0]).Name}", 73 | _ => $"{filter.FilterLanguages.Count} languages" 74 | }, 75 | 76 | (filter.FilterParallelSinglethreaded, filter.FilterParallelMultithreaded) switch 77 | { 78 | (true, false) => "single-threaded", 79 | (false, true) => "multithreaded", 80 | _ => null 81 | }, 82 | 83 | (filter.FilterAlgorithmBase, filter.FilterAlgorithmWheel, filter.FilterAlgorithmOther) switch 84 | { 85 | (false, false, false) => null, 86 | (true, false, false) => "base algorithm", 87 | (false, true, false) => "wheel algorithm", 88 | (false, false, true) => "other algorithms", 89 | (true, true, true) => "all algorithms", 90 | _ => "multiple algorithms" 91 | }, 92 | 93 | (filter.FilterFaithful, filter.FilterUnfaithful) switch 94 | { 95 | (true, false) => "faithful", 96 | (false, true) => "unfaithful", 97 | _ => null 98 | }, 99 | 100 | (filter.FilterBitsOne, filter.FilterBitsOther, filter.FilterBitsUnknown) switch 101 | { 102 | (true, false, false) => "one bit", 103 | (false, true, false) => "multiple bits", 104 | (false, false, true) => "unknown bits", 105 | (true, true, false) => "known bits", 106 | (false, true, true) => "all but one bit", 107 | (true, false, true) => "one or unknown bits", 108 | _ => null 109 | } 110 | ]; 111 | 112 | return string.Join(", ", segments.Where(s => s != null)); 113 | } 114 | 115 | public static IList SplitFilterValues(this string text) 116 | { 117 | return text.Split('~', StringSplitOptions.RemoveEmptyEntries); 118 | } 119 | 120 | public static string JoinFilterValues(this IEnumerable values) 121 | { 122 | return string.Join('~', values); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Frontend/Filters/IResultFilterPropertyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PrimeView.Frontend.Filters 4 | { 5 | public interface IResultFilterPropertyProvider 6 | { 7 | public IList FilterLanguages { get; } 8 | 9 | public bool FilterParallelSinglethreaded { get; } 10 | public bool FilterParallelMultithreaded { get; } 11 | 12 | public bool FilterAlgorithmBase { get; } 13 | public bool FilterAlgorithmWheel { get; } 14 | public bool FilterAlgorithmOther { get; } 15 | 16 | public bool FilterFaithful { get; } 17 | public bool FilterUnfaithful { get; } 18 | 19 | public bool FilterBitsUnknown { get; } 20 | public bool FilterBitsOne { get; } 21 | public bool FilterBitsOther { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Frontend/Filters/LeaderboardFilterPreset.cs: -------------------------------------------------------------------------------- 1 | using PrimeView.Frontend.Tools; 2 | 3 | namespace PrimeView.Frontend.Filters 4 | { 5 | public class LeaderboardFilterPreset : ResultFilterPreset 6 | { 7 | public LeaderboardFilterPreset() 8 | { 9 | Name = "Leaderboard"; 10 | ImplementationText = string.Empty; 11 | ParallelismText = Constants.MultithreadedTag; 12 | AlgorithmText = new string[] { Constants.WheelTag, Constants.OtherTag }.JoinFilterValues(); 13 | FaithfulText = Constants.UnfaithfulTag; 14 | BitsText = new string[] { Constants.UnknownTag, Constants.OtherTag }.JoinFilterValues(); 15 | } 16 | 17 | public override bool IsFixed => true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Frontend/Filters/MultithreadedLeaderboardFilterPreset.cs: -------------------------------------------------------------------------------- 1 | using PrimeView.Frontend.Tools; 2 | 3 | namespace PrimeView.Frontend.Filters 4 | { 5 | public class MultithreadedLeaderboardFilterPreset : ResultFilterPreset 6 | { 7 | public MultithreadedLeaderboardFilterPreset() 8 | { 9 | Name = "Multithreaded leaderboard"; 10 | ImplementationText = string.Empty; 11 | ParallelismText = Constants.SinglethreadedTag; 12 | AlgorithmText = new string[] { Constants.WheelTag, Constants.OtherTag }.JoinFilterValues(); 13 | FaithfulText = Constants.UnfaithfulTag; 14 | BitsText = new string[] { Constants.UnknownTag, Constants.OtherTag }.JoinFilterValues(); 15 | } 16 | 17 | public override bool IsFixed => true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Frontend/Filters/ResultFilterPreset.cs: -------------------------------------------------------------------------------- 1 | using PrimeView.Frontend.Tools; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace PrimeView.Frontend.Filters 6 | { 7 | public class ResultFilterPreset : IResultFilterPropertyProvider 8 | { 9 | [JsonPropertyName("nm")] 10 | public string Name { get; set; } 11 | 12 | [JsonPropertyName("it")] 13 | public string ImplementationText { get; set; } 14 | 15 | [JsonPropertyName("pt")] 16 | public string ParallelismText { get; set; } 17 | 18 | [JsonPropertyName("at")] 19 | public string AlgorithmText { get; set; } 20 | 21 | [JsonPropertyName("ft")] 22 | public string FaithfulText { get; set; } 23 | 24 | [JsonPropertyName("bt")] 25 | public string BitsText { get; set; } 26 | 27 | [JsonIgnore] 28 | public virtual bool IsFixed => false; 29 | 30 | [JsonIgnore] 31 | public IList FilterLanguages 32 | => ImplementationText.SplitFilterValues(); 33 | 34 | [JsonIgnore] 35 | public bool FilterParallelSinglethreaded 36 | => !ParallelismText.SplitFilterValues().Contains(Constants.SinglethreadedTag); 37 | 38 | [JsonIgnore] 39 | public bool FilterParallelMultithreaded 40 | => !ParallelismText.SplitFilterValues().Contains(Constants.MultithreadedTag); 41 | 42 | [JsonIgnore] 43 | public bool FilterAlgorithmBase 44 | => !AlgorithmText.SplitFilterValues().Contains(Constants.BaseTag); 45 | 46 | [JsonIgnore] 47 | public bool FilterAlgorithmWheel 48 | => !AlgorithmText.SplitFilterValues().Contains(Constants.WheelTag); 49 | 50 | [JsonIgnore] 51 | public bool FilterAlgorithmOther 52 | => !AlgorithmText.SplitFilterValues().Contains(Constants.OtherTag); 53 | 54 | [JsonIgnore] 55 | public bool FilterFaithful 56 | => !FaithfulText.SplitFilterValues().Contains(Constants.FaithfulTag); 57 | 58 | [JsonIgnore] 59 | public bool FilterUnfaithful 60 | => !FaithfulText.SplitFilterValues().Contains(Constants.UnfaithfulTag); 61 | 62 | [JsonIgnore] 63 | public bool FilterBitsUnknown 64 | => !BitsText.SplitFilterValues().Contains(Constants.UnknownTag); 65 | 66 | [JsonIgnore] 67 | public bool FilterBitsOne 68 | => !BitsText.SplitFilterValues().Contains(Constants.OneTag); 69 | 70 | [JsonIgnore] 71 | public bool FilterBitsOther 72 | => !BitsText.SplitFilterValues().Contains(Constants.OtherTag); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Frontend/Frontend.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | PrimeView.Frontend 4 | net8.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Frontend/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | @inherits SortedTablePage 4 | 5 | @using BlazorTable 6 | @using PrimeView.Entities 7 | @using PrimeView.Frontend.Sorting 8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |

Reports

29 |
30 |
31 | 32 | @if (this.runners != null) 33 | { 34 |
35 |
36 |
37 |
38 | 39 |
40 | 48 |
49 |
50 |
51 | } 52 | 53 |
54 | 55 | @foreach(int reportNumber in new int[] {25, 50, 100}) 56 | { 57 | int loopNumber = reportNumber; 58 | 59 | 60 | } 61 | 62 | 63 | 64 |
65 | 66 | @if (this.pageCount > 1) 67 | { 68 | // We include 5 pages around and including the current page. We put the current page in the middle, except if we're at one end of the range of pages 69 | int pageIndexStart = this.pageNumber - 2; 70 | int pageIndexEnd = this.pageNumber + 2; 71 | 72 | if (pageIndexStart < 1) 73 | { 74 | pageIndexStart = 1; 75 | pageIndexEnd = Math.Min(this.pageCount, 5); 76 | } 77 | else if (pageIndexEnd > this.pageCount) 78 | { 79 | pageIndexStart = Math.Max(pageCount - 4, 1); 80 | pageIndexEnd = this.pageCount; 81 | } 82 | 83 | 141 | } 142 | 143 | 144 | @{ OnTableRefreshStart(); } 145 | 146 | 149 | 150 | 151 | 152 | 153 | @foreach (var user in this.summaries.Select(r => r.User).Where(u => u != null).Distinct().OrderBy(u => u)) 154 | { 155 | 156 | } 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 176 | 177 | 178 | 181 | 182 |
183 |
184 |
185 | -------------------------------------------------------------------------------- /src/Frontend/Pages/Index.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.Extensions.Configuration; 3 | using PrimeView.Entities; 4 | using PrimeView.Frontend.Parameters; 5 | using PrimeView.Frontend.Tools; 6 | using System.Threading.Tasks; 7 | 8 | namespace PrimeView.Frontend.Pages 9 | { 10 | public partial class Index 11 | { 12 | [Inject] 13 | public IConfiguration Configuration { get; set; } 14 | 15 | [Inject] 16 | public IReportReader ReportReader { get; set; } 17 | 18 | private string filterRunners = string.Empty; 19 | private int reportCount; 20 | 21 | [QueryStringParameter("rc")] 22 | public int ReportCount 23 | { 24 | get => this.reportCount; 25 | set 26 | { 27 | if (value > 0) 28 | this.reportCount = value; 29 | } 30 | } 31 | 32 | private int skipReports; 33 | 34 | [QueryStringParameter("rs")] 35 | public int SkipReports 36 | { 37 | get => this.skipReports; 38 | set 39 | { 40 | if (value >= 0) 41 | this.skipReports = value; 42 | } 43 | } 44 | 45 | [QueryStringParameter("fr")] 46 | public string FilterRunners 47 | { 48 | get => filterRunners; 49 | set 50 | { 51 | filterRunners = value ?? string.Empty; 52 | 53 | // The following smells up to high heaven, and I don't like it. Sadly, using a @bind is the only way I 54 | // could find to reliably link the value that is shown in the runner drop-down (i.e. the selected value) 55 | // to the one that is actually chosen. And in the context of the @bind, this seems to be the way to kick 56 | // off a load of the correct summaries and a rerender, in the background. 57 | if (summaries != null) 58 | { 59 | new Task(async () => 60 | { 61 | await LoadSummaries(); 62 | StateHasChanged(); 63 | }).Start(); 64 | } 65 | } 66 | } 67 | 68 | private Runner[] runners = null; 69 | private ReportSummary[] summaries = null; 70 | private int totalReports = 0; 71 | private int newReportCount; 72 | private int pageNumber = 1; 73 | private int pageCount = 1; 74 | 75 | public override Task SetParametersAsync(ParameterView parameters) 76 | { 77 | ReportCount = Configuration.GetValue(Constants.ReportCount, 50); 78 | SkipReports = 0; 79 | SortColumn = "dt"; 80 | SortDescending = true; 81 | 82 | return base.SetParametersAsync(parameters); 83 | } 84 | 85 | protected override async Task OnInitializedAsync() 86 | { 87 | this.runners = await ReportReader.GetRunners(); 88 | SkipReports -= SkipReports % ReportCount; 89 | await LoadSummaries(); 90 | this.newReportCount = ReportCount; 91 | } 92 | 93 | private async Task ApplyNewReportCount(int reportCount) 94 | { 95 | this.newReportCount = reportCount; 96 | await ApplyNewReportCount(); 97 | } 98 | 99 | private async Task ApplyNewReportCount() 100 | { 101 | ReportCount = this.newReportCount; 102 | SkipReports -= SkipReports % ReportCount; 103 | 104 | if (summaries != null) 105 | await LoadSummaries(); 106 | } 107 | 108 | private async Task ApplyPageNumber(int pageNumber) 109 | { 110 | SkipReports = (pageNumber - 1) * ReportCount; 111 | 112 | if (summaries != null) 113 | await LoadSummaries(); 114 | } 115 | 116 | private async Task LoadSummaries() 117 | { 118 | (this.summaries, this.totalReports) = await ReportReader.GetSummaries(FilterRunners, SkipReports, ReportCount); 119 | 120 | // adjust SkipReports if we're skipping all reports that we have 121 | if (this.totalReports > 0 && SkipReports >= this.totalReports) 122 | { 123 | SkipReports = this.totalReports - 1 - ((this.totalReports - 1) % ReportCount); 124 | (this.summaries, this.totalReports) = await ReportReader.GetSummaries(FilterRunners, SkipReports, ReportCount); 125 | } 126 | 127 | this.pageNumber = this.SkipReports / this.ReportCount + 1; 128 | this.pageCount = this.totalReports / this.ReportCount; 129 | if (this.totalReports % this.ReportCount > 0) 130 | this.pageCount++; 131 | } 132 | 133 | private void LoadReport(string reportId) 134 | { 135 | NavigationManager.NavigateTo($"report?id={reportId}"); 136 | } 137 | 138 | private async Task Refresh() 139 | { 140 | ReportReader.FlushCache(); 141 | this.runners = await ReportReader.GetRunners(); 142 | await LoadSummaries(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Frontend/Pages/ReportDetails.razor: -------------------------------------------------------------------------------- 1 | @page "/report" 2 | 3 | @inherits SortedTablePage 4 | 5 | @using BlazorTable 6 | @using PrimeView.Entities 7 | @using PrimeView.Frontend.Tools 8 | @using PrimeView.Frontend.Filters 9 | @using PrimeView.Frontend.Sorting 10 | 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |

@ReportTitle

33 |
34 | @if (this.report != null) 35 | { 36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 | } 46 |
47 | 48 | @if (this.report == null) 49 | { 50 |
51 |
52 |
53 |
54 |
55 | Loading... 56 |
57 |
58 | } 59 | else 60 | { 61 |
62 |
63 |
64 |

65 | 68 |

69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 |
77 | 78 | @{ 79 | string filterSummary = string.Empty; 80 | 81 | if (HideFilters) 82 | { 83 | filterSummary = $": {this.CreateSummary(this)}"; 84 | if (OnlyHighestPassesPerSecondPerThreadPerLanguage) 85 | filterSummary += ", only show highest passes/s/t"; 86 | } 87 | } 88 |
89 |
90 |

91 | 94 |

95 |
96 | 97 |
98 |
99 | 100 |
101 |
102 |
103 | 104 | 111 |
112 |
113 | 114 |
115 | 116 |
117 | 118 | 121 |
122 |
123 | 124 | 127 |
128 |
129 | 130 |
131 | 132 |
133 | 134 | 137 |
138 |
139 | 140 | 143 |
144 |
145 | 146 | 149 |
150 |
151 | 152 |
153 | 154 |
155 | 156 | 159 |
160 |
161 | 162 | 165 |
166 |
167 | 168 |
169 | 170 |
171 | 172 | 175 |
176 |
177 | 178 | 181 |
182 |
183 | 184 | 187 |
188 |
189 |
190 |
191 |
192 | 193 |
194 |
195 |
196 |
197 |
198 | 199 | 202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 | 210 |
211 |
212 |

213 | 216 |

217 |
218 | 219 |
220 |
    221 | @for (int i = 0; i < (this.filterPresets?.Count ?? 0); i++) 222 | { 223 | int presetIndex = i; 224 | var preset = this.filterPresets[presetIndex]; 225 | 226 |
  • 227 | 230 | 233 |
  • 234 | } 235 |
    236 | 237 | 240 |
    241 |
242 |
243 |
244 | 245 |
246 | 247 | var filteredItems = this.report.Results.Viewable().ApplyFilters(this); 248 | 249 | @{ OnTableRefreshStart(); } 250 | 251 | 254 | 255 | r.Language).Distinct().Count()} languages")"> 256 | 257 | 258 | @foreach (var languageInfo in this.report.Results.Select(r => GetLanguageInfo(r.Language)).Distinct(new LanguageInfo.KeyEqualityComparer()).OrderBy(n => n.Name)) 259 | { 260 | 261 | } 262 | 263 | 264 | 276 | 277 | 278 | 288 | 289 | 290 | 300 | 301 | 302 | 303 | !r.IsMultiThreaded)} single-threaded")" Align="Align.Right" /> 304 | 305 | r.IsMultiThreaded)} multithreaded")" Align="Align.Center"> 306 | 307 | 308 | 309 | 310 | 311 | 312 | 315 | 316 | r.Algorithm == "base")} base")" Align="Align.Center"> 317 | 318 | 319 | @foreach (var algorithm in this.report.Results.Select(r => r.Algorithm).Where(a => a != null).Distinct().OrderBy(a => a)) 320 | { 321 | 322 | } 323 | 324 | 325 | 335 | 336 | r.IsFaithful == true)} faithful")" Align="Align.Center"> 337 | 338 | 339 | 340 | 341 | 342 | 343 | 353 | 354 | r.Bits == 1)} single-bit")" Align="Align.Right"> 355 | 356 | 357 | @foreach (var bitCount in this.report.Results.Select(r => r.Bits).Where(b => b != null).Cast().Distinct().OrderBy(b => b)) 358 | { 359 | 360 | } 361 | 362 | 363 | 377 | 378 |
379 | } 380 |
381 |
382 | -------------------------------------------------------------------------------- /src/Frontend/Pages/ReportDetails.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.JSInterop; 4 | using PrimeView.Entities; 5 | using PrimeView.Frontend.Filters; 6 | using PrimeView.Frontend.Parameters; 7 | using PrimeView.Frontend.ReportExporters; 8 | using PrimeView.Frontend.Sorting; 9 | using PrimeView.Frontend.Tools; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Net.Http; 15 | using System.Net.Http.Json; 16 | using System.Text; 17 | using System.Threading.Tasks; 18 | 19 | namespace PrimeView.Frontend.Pages 20 | { 21 | public partial class ReportDetails : SortedTablePage, IResultFilterPropertyProvider, ILanguageInfoProvider 22 | { 23 | private const string FilterPresetStorageKey = "ResultFilterPresets"; 24 | 25 | [Inject] 26 | public HttpClient Http { get; set; } 27 | 28 | [Inject] 29 | public IConfiguration Configuration { get; set; } 30 | 31 | [Inject] 32 | public IReportReader ReportReader { get; set; } 33 | 34 | [QueryStringParameter("hi")] 35 | public bool HideSystemInformation { get; set; } = false; 36 | 37 | [QueryStringParameter("hf")] 38 | public bool HideFilters { get; set; } = false; 39 | 40 | [QueryStringParameter("hp")] 41 | public bool HideFilterPresets { get; set; } = false; 42 | 43 | [QueryStringParameter("id")] 44 | public string ReportId { get; set; } 45 | 46 | [QueryStringParameter("fi")] 47 | public string FilterLanguageText { get; set; } = string.Empty; 48 | 49 | [QueryStringParameter("fp")] 50 | public string FilterParallelismText 51 | { 52 | get => JoinFilterValueString(!FilterParallelSinglethreaded, Constants.SinglethreadedTag, !FilterParallelMultithreaded, Constants.MultithreadedTag); 53 | 54 | set 55 | { 56 | var values = value.SplitFilterValues(); 57 | 58 | FilterParallelSinglethreaded = !values.Contains(Constants.SinglethreadedTag); 59 | FilterParallelMultithreaded = !values.Contains(Constants.MultithreadedTag); 60 | } 61 | } 62 | 63 | [QueryStringParameter("fa")] 64 | public string FilterAlgorithmText 65 | { 66 | get => JoinFilterValueString(!FilterAlgorithmBase, Constants.BaseTag, !FilterAlgorithmWheel, Constants.WheelTag, !FilterAlgorithmOther, Constants.OtherTag); 67 | 68 | set 69 | { 70 | var values = value.SplitFilterValues(); 71 | 72 | FilterAlgorithmBase = !values.Contains(Constants.BaseTag); 73 | FilterAlgorithmWheel = !values.Contains(Constants.WheelTag); 74 | FilterAlgorithmOther = !values.Contains(Constants.OtherTag); 75 | } 76 | } 77 | 78 | [QueryStringParameter("ff")] 79 | public string FilterFaithfulText 80 | { 81 | get => JoinFilterValueString(!FilterFaithful, Constants.FaithfulTag, !FilterUnfaithful, Constants.UnfaithfulTag); 82 | 83 | set 84 | { 85 | var values = value.SplitFilterValues(); 86 | 87 | FilterFaithful = !values.Contains(Constants.FaithfulTag); 88 | FilterUnfaithful = !values.Contains(Constants.UnfaithfulTag); 89 | } 90 | } 91 | 92 | [QueryStringParameter("fb")] 93 | public string FilterBitsText 94 | { 95 | get => JoinFilterValueString(!FilterBitsUnknown, Constants.UnknownTag, !FilterBitsOne, Constants.OneTag, !FilterBitsOther, Constants.OtherTag); 96 | 97 | set 98 | { 99 | var values = value.SplitFilterValues(); 100 | 101 | FilterBitsUnknown = !values.Contains(Constants.UnknownTag); 102 | FilterBitsOne = !values.Contains(Constants.OneTag); 103 | FilterBitsOther = !values.Contains(Constants.OtherTag); 104 | } 105 | } 106 | 107 | [QueryStringParameter("tp")] 108 | public bool OnlyHighestPassesPerSecondPerThreadPerLanguage { get; set; } = false; 109 | 110 | public IList FilterLanguages 111 | => FilterLanguageText.SplitFilterValues(); 112 | 113 | public bool FilterParallelSinglethreaded { get; set; } = true; 114 | public bool FilterParallelMultithreaded { get; set; } = true; 115 | 116 | public bool FilterAlgorithmBase { get; set; } = true; 117 | public bool FilterAlgorithmWheel { get; set; } = true; 118 | public bool FilterAlgorithmOther { get; set; } = true; 119 | 120 | public bool FilterFaithful { get; set; } = true; 121 | public bool FilterUnfaithful { get; set; } = true; 122 | 123 | public bool FilterBitsUnknown { get; set; } = true; 124 | public bool FilterBitsOne { get; set; } = true; 125 | public bool FilterBitsOther { get; set; } = true; 126 | 127 | private bool AreFiltersClear 128 | => FilterLanguageText == string.Empty 129 | && FilterParallelismText == string.Empty 130 | && FilterAlgorithmText == string.Empty 131 | && FilterFaithfulText == string.Empty 132 | && FilterBitsText == string.Empty; 133 | 134 | private string ReportTitle 135 | { 136 | get 137 | { 138 | StringBuilder titleBuilder = new(); 139 | 140 | if (report?.User != null) 141 | titleBuilder.Append($" by {report.User}"); 142 | 143 | if (report?.Date != null) 144 | titleBuilder.Append($" at {report.Date.Value.ToLocalTime()}"); 145 | 146 | return titleBuilder.Length > 0 ? $"Report generated{titleBuilder}" : "Report"; 147 | } 148 | } 149 | 150 | private string ReportFileBaseName 151 | { 152 | get 153 | { 154 | StringBuilder fileNameBuilder = new("primes_report"); 155 | 156 | if (report?.User != null) 157 | fileNameBuilder.Append($"_{report.User.Replace(' ', '_')}"); 158 | 159 | if (report?.Date != null) 160 | fileNameBuilder.Append($"_{report.Date.Value.ToLocalTime().ToString().Replace(' ', '_')}"); 161 | 162 | return fileNameBuilder.ToString(); 163 | } 164 | } 165 | 166 | private string solutionUrlTemplate; 167 | private Report report = null; 168 | private int rowNumber = 0; 169 | private Dictionary languageMap = null; 170 | private List filterPresets = null; 171 | private string filterPresetName; 172 | private ElementReference languagesSelect; 173 | 174 | public override Task SetParametersAsync(ParameterView parameters) 175 | { 176 | SortColumn = "pp"; 177 | SortDescending = true; 178 | 179 | return base.SetParametersAsync(parameters); 180 | } 181 | 182 | protected override async Task OnInitializedAsync() 183 | { 184 | this.solutionUrlTemplate = Configuration.GetValue(Constants.SolutionUrlTemplate, null); 185 | this.report = await ReportReader.GetReport(ReportId); 186 | await LoadLanguageMap(); 187 | 188 | if (this.solutionUrlTemplate != null) 189 | { 190 | foreach (var result in this.report.Results) 191 | { 192 | var languageInfo = GetLanguageInfo(result.Language); 193 | 194 | result.SolutionUrl = this.solutionUrlTemplate 195 | .Replace("{sln}", result.Solution) 196 | .Replace("{tag}", languageInfo.Tag ?? languageInfo.Name); 197 | } 198 | } 199 | 200 | if (LocalStorage.ContainKey(FilterPresetStorageKey)) 201 | { 202 | try 203 | { 204 | this.filterPresets = LocalStorage.GetItem>(FilterPresetStorageKey); 205 | } 206 | catch 207 | { 208 | LocalStorage.RemoveItem(FilterPresetStorageKey); 209 | } 210 | } 211 | 212 | this.filterPresets ??= []; 213 | 214 | InsertFilterPreset(new LeaderboardFilterPreset()); 215 | InsertFilterPreset(new MultithreadedLeaderboardFilterPreset()); 216 | 217 | await base.OnInitializedAsync(); 218 | } 219 | 220 | private async Task ClearFilters() 221 | { 222 | FilterLanguageText = string.Empty; 223 | FilterParallelismText = string.Empty; 224 | FilterAlgorithmText = string.Empty; 225 | FilterFaithfulText = string.Empty; 226 | FilterBitsText = string.Empty; 227 | 228 | await JSRuntime.InvokeVoidAsync("PrimeViewJS.ClearMultiselectValues", languagesSelect); 229 | } 230 | 231 | private void ToggleSystemInfoPanel() 232 | { 233 | HideSystemInformation = !HideSystemInformation; 234 | } 235 | 236 | private void ToggleFilterPanel() 237 | { 238 | HideFilters = !HideFilters; 239 | } 240 | 241 | private void ToggleFilterPresetPanel() 242 | { 243 | HideFilterPresets = !HideFilterPresets; 244 | } 245 | 246 | private async Task LoadLanguageMap() 247 | { 248 | try 249 | { 250 | this.languageMap = await Http.GetFromJsonAsync>("data/langmap.json"); 251 | foreach (var entry in languageMap) 252 | { 253 | entry.Value.Key = entry.Key; 254 | } 255 | } 256 | catch { } 257 | } 258 | 259 | protected override void OnTableRefreshStart() 260 | { 261 | rowNumber = this.sortedTable.PageNumber * this.sortedTable.PageSize; 262 | 263 | base.OnTableRefreshStart(); 264 | } 265 | 266 | public LanguageInfo GetLanguageInfo(string language) 267 | { 268 | this.languageMap ??= []; 269 | 270 | if (languageMap.TryGetValue(language, out LanguageInfo value)) 271 | return value; 272 | 273 | LanguageInfo info = new() { Key = language, Name = language[0].ToString().ToUpper() + language[1..] }; 274 | 275 | this.languageMap[language] = info; 276 | 277 | return info; 278 | } 279 | 280 | private async Task LanguageSelectionChanged() 281 | { 282 | FilterLanguageText = await JSRuntime.InvokeAsync("PrimeViewJS.GetMultiselectValues", languagesSelect, "~") ?? string.Empty; 283 | } 284 | 285 | private static string JoinFilterValueString(params object[] flagSet) 286 | { 287 | List setFlags = []; 288 | 289 | for (int i = 0; i < flagSet.Length; i += 2) 290 | { 291 | if ((bool)flagSet[i]) 292 | setFlags.Add(flagSet[i + 1].ToString()); 293 | } 294 | 295 | return setFlags.JoinFilterValues(); 296 | } 297 | 298 | private bool IsFilterPresetNameValid(string name) 299 | { 300 | return !string.IsNullOrWhiteSpace(name) 301 | && (filterPresets == null || !filterPresets.Any(preset => preset.IsFixed && string.Equals(preset.Name, name, StringComparison.OrdinalIgnoreCase))); 302 | } 303 | 304 | private async Task ApplyFilterPreset(int index) 305 | { 306 | var preset = this.filterPresets?[index]; 307 | 308 | if (preset == null) 309 | return; 310 | 311 | FilterAlgorithmText = preset.AlgorithmText; 312 | FilterBitsText = preset.BitsText; 313 | FilterFaithfulText = preset.FaithfulText; 314 | FilterLanguageText = preset.ImplementationText; 315 | FilterParallelismText = preset.ParallelismText; 316 | 317 | var filterImplementations = FilterLanguages; 318 | 319 | if (filterImplementations.Count > 0) 320 | await JSRuntime.InvokeVoidAsync("PrimeViewJS.SetMultiselectValues", languagesSelect, FilterLanguages.ToArray()); 321 | 322 | else 323 | await JSRuntime.InvokeVoidAsync("PrimeViewJS.ClearMultiselectValues", languagesSelect); 324 | 325 | this.filterPresetName = preset.IsFixed ? string.Empty : preset.Name; 326 | } 327 | 328 | private void RemoveFilterPreset(int index) 329 | { 330 | this.filterPresets?.RemoveAt(index); 331 | 332 | SaveFilterPresets(); 333 | } 334 | 335 | private void InsertFilterPreset(ResultFilterPreset preset) 336 | { 337 | this.filterPresets ??= []; 338 | 339 | int i; 340 | for (i = 0; i < this.filterPresets.Count && string.Compare(preset.Name, this.filterPresets[i].Name, StringComparison.OrdinalIgnoreCase) > 0; i++) ; 341 | 342 | if (i < this.filterPresets.Count && string.Equals(preset.Name, this.filterPresets[i].Name, StringComparison.OrdinalIgnoreCase)) 343 | { 344 | if (this.filterPresets[i].IsFixed) 345 | return; 346 | 347 | this.filterPresets.RemoveAt(i); 348 | } 349 | 350 | this.filterPresets.Insert(i, preset); 351 | 352 | SaveFilterPresets(); 353 | } 354 | 355 | private void AddFilterPreset() 356 | { 357 | if (string.IsNullOrWhiteSpace(this.filterPresetName)) 358 | return; 359 | 360 | this.filterPresetName = this.filterPresetName.Trim(); 361 | 362 | InsertFilterPreset(new() 363 | { 364 | Name = this.filterPresetName, 365 | AlgorithmText = FilterAlgorithmText, 366 | BitsText = FilterBitsText, 367 | FaithfulText = FilterFaithfulText, 368 | ImplementationText = FilterLanguageText, 369 | ParallelismText = FilterParallelismText 370 | }); 371 | 372 | this.filterPresetName = null; 373 | } 374 | 375 | private void SaveFilterPresets() 376 | { 377 | LocalStorage.SetItem(FilterPresetStorageKey, filterPresets.Where(preset => !preset.IsFixed)); 378 | } 379 | 380 | private async Task DownloadExport(string fileExtension, string mimeType, byte[] export) 381 | { 382 | using DotNetStreamReference streamRef = new(new MemoryStream(export)); 383 | 384 | await JSRuntime.InvokeVoidAsync("PrimeViewJS.DownloadFileFromStream", ReportFileBaseName + fileExtension, mimeType, streamRef); 385 | } 386 | 387 | private async Task DownloadJson() 388 | { 389 | byte[] export = JsonConverter.Convert(this.report); 390 | 391 | if (export != null) 392 | await DownloadExport(".json", "application/json", export); 393 | } 394 | 395 | private async Task DownloadExcel() 396 | { 397 | byte[] export = ExcelConverter.Convert(this.report, this); 398 | 399 | if (export != null) 400 | await DownloadExport(".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", export); 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/Frontend/Parameters/PropertyParameterMap.cs: -------------------------------------------------------------------------------- 1 | using PrimeView.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace PrimeView.Frontend.Parameters 8 | { 9 | public static class PropertyParameterMap 10 | { 11 | private static readonly Dictionary map; 12 | 13 | static PropertyParameterMap() 14 | { 15 | string result = nameof(Result); 16 | string reportSummary = nameof(ReportSummary); 17 | 18 | map = new() 19 | { 20 | { $"{result}.{nameof(Result.Language)}", "im" }, 21 | { $"{result}.{nameof(Result.Solution)}", "so" }, 22 | { $"{result}.{nameof(Result.Label)}", "la" }, 23 | { $"{result}.{nameof(Result.Passes)}", "ps" }, 24 | { $"{result}.{nameof(Result.Duration)}", "du" }, 25 | { $"{result}.{nameof(Result.Threads)}", "td" }, 26 | { $"{result}.{nameof(Result.Algorithm)}", "al" }, 27 | { $"{result}.{nameof(Result.IsFaithful)}", "ff" }, 28 | { $"{result}.{nameof(Result.Bits)}", "bt" }, 29 | { $"{result}.{nameof(Result.PassesPerSecond)}", "pp" }, 30 | { $"{result}.{nameof(Result.IsMultiThreaded)}", "mt" }, 31 | { $"{reportSummary}.{nameof(ReportSummary.Date)}", "dt" }, 32 | { $"{reportSummary}.{nameof(ReportSummary.User)}", "us" }, 33 | { $"{reportSummary}.{nameof(ReportSummary.CpuVendor)}", "cv" }, 34 | { $"{reportSummary}.{nameof(ReportSummary.CpuBrand)}", "cb" }, 35 | { $"{reportSummary}.{nameof(ReportSummary.CpuCores)}", "cc" }, 36 | { $"{reportSummary}.{nameof(ReportSummary.CpuProcessors)}", "cp" }, 37 | { $"{reportSummary}.{nameof(ReportSummary.OsDistro)}", "od" }, 38 | { $"{reportSummary}.{nameof(ReportSummary.OsPlatform)}", "op" }, 39 | { $"{reportSummary}.{nameof(ReportSummary.OsRelease)}", "or" }, 40 | { $"{reportSummary}.{nameof(ReportSummary.Architecture)}", "ar" }, 41 | { $"{reportSummary}.{nameof(ReportSummary.IsSystemVirtual)}", "sv" }, 42 | { $"{reportSummary}.{nameof(ReportSummary.DockerArchitecture)}", "da" }, 43 | { $"{reportSummary}.{nameof(ReportSummary.ResultCount)}", "rc" } 44 | }; 45 | } 46 | 47 | public static string GetPropertyParameterName(string propertyName) 48 | { 49 | if (propertyName == null) 50 | return null; 51 | 52 | string name = $"{typeof(T).Name}.{propertyName}"; 53 | 54 | return map.TryGetValue(name, out string value) ? value : null; 55 | } 56 | 57 | public static string GetPropertyParameterName(this Expression> expression) 58 | { 59 | if (expression == null) 60 | return null; 61 | 62 | if (expression.Body is not MemberExpression body) 63 | { 64 | UnaryExpression ubody = (UnaryExpression)expression.Body; 65 | body = ubody.Operand as MemberExpression; 66 | } 67 | 68 | MemberInfo memberInfo = body?.Member; 69 | 70 | if (memberInfo == null) 71 | return null; 72 | 73 | string name = $"{memberInfo.DeclaringType.Name}.{memberInfo.Name}"; 74 | 75 | return map.TryGetValue(name, out string value) ? value : null; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Frontend/Parameters/QueryStringParameterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PrimeView.Frontend.Parameters 4 | { 5 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 6 | public sealed class QueryStringParameterAttribute : Attribute 7 | { 8 | public QueryStringParameterAttribute() { } 9 | 10 | public QueryStringParameterAttribute(string name) 11 | { 12 | Name = name; 13 | } 14 | 15 | public string Name { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Frontend/Parameters/QueryStringParameterExtensions.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.WebUtilities; 4 | using Microsoft.Extensions.Primitives; 5 | using Microsoft.JSInterop; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Globalization; 9 | using System.Reflection; 10 | 11 | namespace PrimeView.Frontend.Parameters 12 | { 13 | public static class QueryStringParameterExtensions 14 | { 15 | // Apply the values from the query string to the current component 16 | public static void SetParametersFromQueryString(this ComponentBase component, NavigationManager navigationManager, ISyncLocalStorageService localStorage) 17 | { 18 | if (!Uri.TryCreate(navigationManager.Uri, UriKind.RelativeOrAbsolute, out var uri)) 19 | return; 20 | 21 | // Parse the query string 22 | Dictionary queryString = QueryHelpers.ParseQuery(uri.Query); 23 | Dictionary storedParameters = null; 24 | 25 | string storageKey = GetLocalStorageKey(component); 26 | 27 | if (localStorage.ContainKey(storageKey)) 28 | { 29 | try 30 | { 31 | storedParameters = localStorage.GetItem>(storageKey); 32 | } 33 | catch 34 | { 35 | localStorage.RemoveItem(storageKey); 36 | } 37 | } 38 | 39 | storedParameters ??= []; 40 | 41 | // Enumerate all properties of the component 42 | foreach (var property in GetProperties(component)) 43 | { 44 | // Get the name of the parameter to read from the query string 45 | var parameterName = GetQueryStringParameterName(property); 46 | if (parameterName == null) 47 | continue; // The property is not decorated by [QueryStringParameterAttribute] 48 | 49 | if (!queryString.TryGetValue(parameterName, out StringValues value) && storedParameters.TryGetValue(parameterName, out string storedValue)) 50 | value = storedValue; 51 | 52 | if (!StringValues.IsNullOrEmpty(value)) 53 | { 54 | // Convert the value from string to the actual property type 55 | var convertedValue = ConvertValue(value, property.PropertyType); 56 | property.SetValue(component, convertedValue); 57 | } 58 | } 59 | } 60 | 61 | // Apply the values from the component to the query string 62 | public static void UpdateQueryString(this ComponentBase component, NavigationManager navigationManager, ISyncLocalStorageService localStorage, IJSInProcessRuntime runtime) 63 | { 64 | if (!Uri.TryCreate(navigationManager.Uri, UriKind.RelativeOrAbsolute, out var uri)) 65 | uri = new Uri(navigationManager.Uri); 66 | 67 | // Fill the dictionary with the parameters of the component 68 | Dictionary parameters = QueryHelpers.ParseQuery(uri.Query); 69 | Dictionary storedParameters = []; 70 | 71 | foreach (var property in GetProperties(component)) 72 | { 73 | var parameterName = GetQueryStringParameterName(property); 74 | if (parameterName == null) 75 | continue; 76 | 77 | var value = property.GetValue(component); 78 | 79 | if (value is null) 80 | parameters.Remove(parameterName); 81 | 82 | else 83 | { 84 | var convertedValue = ConvertToString(value); 85 | parameters[parameterName] = convertedValue; 86 | storedParameters[parameterName] = convertedValue; 87 | } 88 | } 89 | 90 | // Compute the new URL 91 | var newUri = uri.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.UriEscaped); 92 | foreach (var parameter in parameters) 93 | { 94 | foreach (var value in parameter.Value) 95 | newUri = QueryHelpers.AddQueryString(newUri, parameter.Key, value); 96 | } 97 | 98 | runtime.InvokeVoid("PrimeViewJS.ShowUrl", newUri); 99 | localStorage.SetItem(GetLocalStorageKey(component), storedParameters); 100 | } 101 | 102 | private static string GetLocalStorageKey(ComponentBase component) 103 | { 104 | return $"{component.GetType().Name}QueryParameters"; 105 | } 106 | 107 | private static object ConvertValue(StringValues value, Type type) 108 | { 109 | try 110 | { 111 | return Convert.ChangeType(value[0], type, CultureInfo.InvariantCulture); 112 | } 113 | catch { } 114 | 115 | return GetDefault(type); 116 | } 117 | 118 | private static object GetDefault(Type type) 119 | { 120 | return type.IsValueType ? Activator.CreateInstance(type) : null; 121 | } 122 | 123 | private static string ConvertToString(object value) 124 | { 125 | return Convert.ToString(value, CultureInfo.InvariantCulture); 126 | } 127 | 128 | private static PropertyInfo[] GetProperties(ComponentBase component) 129 | { 130 | return component.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 131 | } 132 | 133 | private static string GetQueryStringParameterName(PropertyInfo property) 134 | { 135 | var attribute = property.GetCustomAttribute(); 136 | if (attribute == null) 137 | return null; 138 | 139 | return attribute.Name ?? property.Name; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Frontend/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using BlazorTable; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.JSInterop; 7 | using PrimeView.JsonFileReader; 8 | using PrimeView.RestAPIReader; 9 | using System; 10 | using System.Net.Http; 11 | using System.Threading.Tasks; 12 | 13 | using Constants = PrimeView.Frontend.Tools.Constants; 14 | 15 | namespace PrimeView.Frontend 16 | { 17 | public class Program 18 | { 19 | public static async Task Main(string[] args) 20 | { 21 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 22 | builder.RootComponents.Add("#app"); 23 | 24 | string baseAddress = builder.HostEnvironment.BaseAddress; 25 | 26 | builder.Services 27 | .AddScoped(sp => new HttpClient { BaseAddress = new Uri(baseAddress) }) 28 | .AddBlazorTable() 29 | .AddBlazoredLocalStorage() 30 | .AddSingleton(services => (IJSInProcessRuntime)services.GetRequiredService()); 31 | 32 | switch (builder.Configuration.GetValue(Constants.ActiveReader)) 33 | { 34 | case Constants.JsonFileReader: 35 | builder.Services.AddJsonFileReportReader(baseAddress, builder.Configuration.GetSection(Constants.Readers).GetSection(Constants.JsonFileReader)); 36 | break; 37 | 38 | case Constants.RestAPIReader: 39 | builder.Services.AddRestAPIReportReader(builder.Configuration.GetSection(Constants.Readers).GetSection(Constants.RestAPIReader)); 40 | break; 41 | } 42 | 43 | await builder.Build().RunAsync(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Frontend/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:20153", 7 | "sslPort": 44329 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Frontend": { 20 | "commandName": "Project", 21 | "dotnetRunMessages": "true", 22 | "launchBrowser": true, 23 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Frontend/README.md: -------------------------------------------------------------------------------- 1 | # Implementation notes 2 | 3 | * The binding of sorting-related query parameters to the tables on [`Index`](Pages/Index.razor) and [`ReportDetails`](Pages/ReportDetails.razor) pages is largely implemented via the [`SortedTablePage`](Sorting/SortedTablePage.razor) page that the other two inherit from. The tables are connected to `SortedTablePage` using a `@ref` to `SortedTablePage.sortedTable`, an `@onclick` calling `SortedTablePage.FlagTableSortingChange` and an invocation of `SortedTablePage.OnTableRefreshStart()` immediately after the `` opening tag. 4 | 5 | * Mapping of [`Result`](../Entities/Result.cs) and [`ReportSummary`](../Entities/ReportSummary.cs) properties to query parameter values for sorting is done via [`PropertyParameterMap`](Parameters/PropertyParameterMap.cs). If other fields are added to either class that are also included in the respective table, `PropertyParameterMap` would thus need to be extended. 6 | 7 | * To ensure that filter settings on the `ReportDetails` page are properly mapped to and from query parameters, the query parameter (textual) value properties are used to interact with the underlying fields. Those underlying fields are only used to bind the page's controls to that are used to manipulate the settings, in the Filter panel. This means that any (future) filter fields that are not translated to query parameter values, will also not be included in filter presets. 8 | -------------------------------------------------------------------------------- /src/Frontend/ReportExporters/ExcelConverter.cs: -------------------------------------------------------------------------------- 1 | using OfficeOpenXml; 2 | using OfficeOpenXml.Style; 3 | using PrimeView.Entities; 4 | using PrimeView.Frontend.Tools; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Drawing; 8 | using System.Linq; 9 | 10 | namespace PrimeView.Frontend.ReportExporters 11 | { 12 | public static class ExcelConverter 13 | { 14 | public static byte[] Convert(Report report, ILanguageInfoProvider languageInfoProvider) 15 | { 16 | ExcelPackage.LicenseContext = LicenseContext.NonCommercial; 17 | 18 | using ExcelPackage excelPackage = new(); 19 | 20 | var reportSheet = excelPackage.Workbook.Worksheets.Add($"Report"); 21 | FillReportSheet(reportSheet, report); 22 | 23 | var resultSheet = excelPackage.Workbook.Worksheets.Add($"Results"); 24 | FillResultsSheet(resultSheet, report.Results, languageInfoProvider); 25 | 26 | return excelPackage.GetAsByteArray(); 27 | } 28 | 29 | private static void FillReportSheet(ExcelWorksheet sheet, Report report) 30 | { 31 | int rowNumber = 1; 32 | 33 | AddGeneralSection(sheet, ref rowNumber, report); 34 | AddCPUSection(sheet, ref rowNumber, report.CPU); 35 | AddOperatingSystemSection(sheet, ref rowNumber, report.OperatingSystem); 36 | AddSystemSection(sheet, ref rowNumber, report.System); 37 | AddDockerSection(sheet, ref rowNumber, report.DockerInfo); 38 | 39 | sheet.Column(1).Style.Font.Bold = true; 40 | sheet.Column(2).Style.HorizontalAlignment = ExcelHorizontalAlignment.Left; 41 | sheet.Columns.Style.VerticalAlignment = ExcelVerticalAlignment.Top; 42 | sheet.Columns.AutoFit(); 43 | } 44 | 45 | private static void AddGeneralSection(ExcelWorksheet sheet, ref int rowNumber, Report report) 46 | { 47 | AddExpandableSection(sheet, ref rowNumber, "General", (sheet, rowNumber) => 48 | { 49 | int sectionTop = rowNumber; 50 | if (AddValue(sheet, ref rowNumber, "Id", report.Id)) 51 | { 52 | sheet.Rows[sectionTop, rowNumber - 1].Hidden = true; 53 | } 54 | AddValue(sheet, ref rowNumber, "User", report.User); 55 | AddValue(sheet, ref rowNumber, "Created at", report.Date, format: "yyyy-mm-dd HH:MM:SS"); 56 | return rowNumber; 57 | }); 58 | } 59 | 60 | private static void AddCPUSection(ExcelWorksheet sheet, ref int rowNumber, CPUInfo cpu) 61 | { 62 | AddExpandableSection(sheet, ref rowNumber, "CPU", (sheet, rowNumber) => 63 | { 64 | AddValues(sheet, ref rowNumber, new() 65 | { 66 | { "Manufacturer", cpu.Manufacturer }, 67 | { "Raspberry processor", cpu.RaspberryProcessor }, 68 | { "Brand", cpu.Brand }, 69 | { "Vendor", cpu.Vendor }, 70 | { "Family", cpu.Family }, 71 | { "Model", cpu.Model }, 72 | { "Stepping", cpu.Stepping }, 73 | { "Revision", cpu.Revision }, 74 | { "# Cores", cpu.Cores }, 75 | { "# Efficiency cores", cpu.EfficiencyCores }, 76 | { "# Performance cores", cpu.PerformanceCores }, 77 | { "# Physical cores", cpu.PhysicalCores }, 78 | { "# Processors", cpu.Processors }, 79 | { "Speed", cpu.Speed }, 80 | { "Minimum speed", cpu.MinimumSpeed }, 81 | { "Maximum speed", cpu.MaximumSpeed }, 82 | { "Voltage", cpu.Voltage }, 83 | { "Governor", cpu.Governor }, 84 | { "Socket", cpu.Socket } 85 | }); 86 | if (cpu.FlagValues != null) 87 | AddValue(sheet, ref rowNumber, "Flags", string.Join(", ", cpu.FlagValues.OrderBy(f => f)), wordWrap: true); 88 | AddValue(sheet, ref rowNumber, "Virtualization", cpu.Virtualization); 89 | if (cpu.Cache != null && cpu.Cache.Count > 0) 90 | { 91 | AddValue(sheet, ref rowNumber, "Cache", force: true); 92 | foreach (var cacheLine in cpu.Cache) 93 | AddValue(sheet, ref rowNumber, $"- {cacheLine.Key}", cacheLine.Value); 94 | } 95 | return rowNumber; 96 | }); 97 | } 98 | 99 | private static void AddOperatingSystemSection(ExcelWorksheet sheet, ref int rowNumber, OperatingSystemInfo os) 100 | { 101 | AddExpandableValuesSection(sheet, ref rowNumber, "Operating System", new() 102 | { 103 | { "Platform", os.Platform }, 104 | { "Distribution", os.Distribution }, 105 | { "Release", os.Release }, 106 | { "Code name", os.CodeName }, 107 | { "Kernel", os.Kernel }, 108 | { "Architecture", os.Architecture }, 109 | { "Code page", os.CodePage }, 110 | { "Logo file", os.LogoFile }, 111 | { "Build", os.Build }, 112 | { "Service pack", os.ServicePack }, 113 | { "UEFI", os.IsUefi } 114 | }); 115 | } 116 | 117 | private static void AddSystemSection(ExcelWorksheet sheet, ref int rowNumber, SystemInfo system) 118 | { 119 | AddExpandableValuesSection(sheet, ref rowNumber, "System", new() 120 | { 121 | { "Manufacturer", system.Manufacturer }, 122 | { "Raspberry manufacturer", system.RaspberryManufacturer }, 123 | { "SKU", system.SKU }, 124 | { "Virtual", system.IsVirtual }, 125 | { "Model", system.Model }, 126 | { "Version", system.Version }, 127 | { "Raspberry type", system.RaspberryType }, 128 | { "Raspberry revision", system.RaspberryRevision } 129 | }); 130 | } 131 | 132 | private static void AddDockerSection(ExcelWorksheet sheet, ref int rowNumber, DockerInfo docker) 133 | { 134 | AddExpandableValuesSection(sheet, ref rowNumber, "Docker", new() 135 | { 136 | { "Kernel version", docker.KernelVersion }, 137 | { "Operating system", docker.OperatingSystem }, 138 | { "OS version", docker.OSVersion }, 139 | { "OS type", docker.OSType }, 140 | { "Architecture", docker.Architecture }, 141 | { "# CPUs", docker.CPUCount }, 142 | { "Total memory", docker.TotalMemory }, 143 | { "Server version", docker.ServerVersion } 144 | }); 145 | } 146 | 147 | private static void AddExpandableValuesSection(ExcelWorksheet sheet, ref int rowNumber, string title, List> entries) 148 | { 149 | AddExpandableSection(sheet, ref rowNumber, title, (sheet, rowNumber) => 150 | { 151 | AddValues(sheet, ref rowNumber, entries); 152 | return rowNumber; 153 | }); 154 | } 155 | 156 | private static void AddExpandableSection(ExcelWorksheet sheet, ref int rowNumber, string title, Func AddEntries) 157 | { 158 | var cell = sheet.Cells[rowNumber, 1]; 159 | 160 | cell.Value = title; 161 | cell.Style.Font.Size = 14; 162 | cell.Style.Font.UnderLine = true; 163 | 164 | rowNumber++; 165 | 166 | int sectionTop = rowNumber; 167 | rowNumber = AddEntries(sheet, rowNumber); 168 | 169 | int sectionBottom = rowNumber - 1; 170 | 171 | rowNumber++; 172 | 173 | if (sectionBottom <= sectionTop) 174 | return; 175 | 176 | var rowRange = sheet.Rows[sectionTop, sectionBottom]; 177 | rowRange.OutlineLevel = 1; 178 | rowRange.Collapsed = false; 179 | } 180 | 181 | private static bool AddValues(ExcelWorksheet sheet, ref int rowNumber, List> entries) 182 | { 183 | bool result = false; 184 | foreach (var entry in entries) 185 | result |= AddValue(sheet, ref rowNumber, entry.Key, entry.Value); 186 | return result; 187 | } 188 | 189 | private static bool AddValue(ExcelWorksheet sheet, ref int rowNumber, string label, object value = null, bool force = false, string format = null, bool wordWrap = false) 190 | { 191 | if (!force && ((value is string stringValue && string.IsNullOrEmpty(stringValue)) || (value is not string && value == null))) 192 | return false; 193 | 194 | sheet.Cells[rowNumber, 1].Value = label + ':'; 195 | var valueCell = sheet.Cells[rowNumber, 2]; 196 | valueCell.Value = value; 197 | if (format != null) 198 | valueCell.Style.Numberformat.Format = format; 199 | if (wordWrap) 200 | valueCell.Style.WrapText = true; 201 | 202 | rowNumber++; 203 | return true; 204 | } 205 | 206 | private static void FillResultsSheet(ExcelWorksheet sheet, IEnumerable results, ILanguageInfoProvider languageInfoProvider) 207 | { 208 | sheet.Cells.LoadFromCollection(results); 209 | int lastRow = sheet.Dimension.Rows - 1; 210 | for (int i = 2; i <= lastRow; i++) 211 | { 212 | var languageInfo = languageInfoProvider.GetLanguageInfo(sheet.Cells[i, Result.LanguageColumnIndex].Text); 213 | 214 | var cell = sheet.Cells[i, Result.LanguageColumnIndex]; 215 | cell.Value = languageInfo.Name; 216 | ExcelFont font; 217 | 218 | if (!string.IsNullOrEmpty(languageInfo.URL)) 219 | { 220 | cell.Hyperlink = new Uri(languageInfo.URL); 221 | font = cell.Style.Font; 222 | font.Color.SetColor(Color.Blue); 223 | font.UnderLine = true; 224 | } 225 | 226 | var uriText = sheet.Cells[i, Result.SolutionUriColumnIndex].Text; 227 | if (!string.IsNullOrEmpty(uriText)) 228 | { 229 | cell = sheet.Cells[i, Result.SolutionColumnIndex]; 230 | cell.Hyperlink = new Uri(uriText); 231 | font = cell.Style.Font; 232 | font.Color.SetColor(Color.Blue); 233 | font.UnderLine = true; 234 | } 235 | 236 | bool allgreen = true; 237 | 238 | cell = sheet.Cells[i, Result.AlgorithmColumnIndex]; 239 | if (cell.Value as string == "base") 240 | cell.Style.Font.Color.SetColor(Color.Green); 241 | else 242 | allgreen = false; 243 | 244 | cell = sheet.Cells[i, Result.IsFaithfulColumnIndex]; 245 | if (cell.Value as bool? ?? false) 246 | cell.Style.Font.Color.SetColor(Color.Green); 247 | else 248 | allgreen = false; 249 | 250 | cell = sheet.Cells[i, Result.BitsColumnIndex]; 251 | if (cell.Value is int value && value == 1) 252 | cell.Style.Font.Color.SetColor(Color.Green); 253 | else 254 | allgreen = false; 255 | 256 | if (allgreen) 257 | sheet.Cells[i, Result.LabelColumnIndex].Style.Font.Color.SetColor(Color.Green); 258 | } 259 | 260 | sheet.Cells[2, Result.SolutionColumnIndex, lastRow, Result.SolutionColumnIndex].Style.HorizontalAlignment = ExcelHorizontalAlignment.Right; 261 | sheet.Column(Result.SolutionUriColumnIndex).Hidden = true; 262 | sheet.Cells[2, Result.IsMultiThreadedColumnIndex, lastRow, Result.IsMultiThreadedColumnIndex].Style.Font.Color.SetColor(Color.Green); 263 | sheet.Columns.AutoFit(); 264 | } 265 | 266 | public static void Add(this List> pairList, T1 key, T2 value) 267 | { 268 | pairList.Add(new KeyValuePair(key, value)); 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/Frontend/ReportExporters/JsonConverter.cs: -------------------------------------------------------------------------------- 1 | using PrimeView.Entities; 2 | using System.Text; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace PrimeView.Frontend.ReportExporters 7 | { 8 | public static class JsonConverter 9 | { 10 | public static byte[] Convert(Report report) 11 | { 12 | string jsonValue; 13 | try 14 | { 15 | jsonValue = JsonSerializer.Serialize(report, options: new() 16 | { 17 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 18 | WriteIndented = true, 19 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 20 | }); 21 | } 22 | catch 23 | { 24 | return null; 25 | } 26 | 27 | return Encoding.UTF8.GetBytes(jsonValue); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Frontend/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | @Body 5 |
6 | -------------------------------------------------------------------------------- /src/Frontend/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Frontend/Shared/SystemInformation.razor: -------------------------------------------------------------------------------- 1 | @using PrimeView.Entities; 2 | 3 | @{ 4 | var cpu = Report.CPU; 5 | var os = Report.OperatingSystem; 6 | var system = Report.System; 7 | var docker = Report.DockerInfo; 8 | } 9 | 10 |
11 |
12 |
13 | @if (cpu != null) 14 | { 15 | CPU 16 | } 17 | @if (os != null) 18 | { 19 | Operating System 20 | } 21 | @if (system != null) 22 | { 23 | System 24 | } 25 | @if (docker != null) 26 | { 27 | Docker 28 | } 29 |
30 |
31 |
32 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | @if (cpu.FlagValues != null) 66 | { 67 | 68 | 69 | 70 | 71 | } 72 | 73 | @if (cpu.Cache != null && cpu.Cache.Count > 0) 74 | { 75 | 76 | 77 | 90 | 91 | } 92 | 93 |
Flags:@string.Join(", ", cpu.FlagValues.OrderBy(f => f))
Cache: 78 | 79 | 80 | @foreach (var cacheLine in cpu.Cache) 81 | { 82 | 83 | 84 | @cacheLine.Value 85 | 86 | } 87 | 88 |
@(cacheLine.Key):
89 |
94 |
95 | 96 | 97 | } 98 | @if (os != null) 99 | { 100 |
101 |
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 |
114 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 |
125 |
126 |
127 | } 128 | @if (system != null) 129 | { 130 |
131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
141 |
142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 |
152 |
153 |
154 | } 155 | @if (docker != null) 156 | { 157 |
158 |
159 |
160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
168 |
169 |
170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
178 |
179 |
180 |
181 | } 182 | 183 | 184 | 185 | 186 | @code 187 | { 188 | [Parameter] 189 | public Report Report { get; set; } 190 | } 191 | -------------------------------------------------------------------------------- /src/Frontend/Shared/ValueTableRow.razor: -------------------------------------------------------------------------------- 1 | @if ((Value is string stringValue && !string.IsNullOrEmpty(stringValue)) || (Value is not string && Value != null)) 2 | { 3 | 4 | @(Label): 5 | 6 | @(Value switch 7 | { 8 | bool boolValue => boolValue ? "yes" : "no", 9 | _ => Value.ToString() 10 | }) 11 | 12 | 13 | } 14 | 15 | @code { 16 | [Parameter] 17 | public object Value { get; set; } 18 | 19 | [Parameter] 20 | public string Label { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/Frontend/Shared/ValueTableRow.razor.css: -------------------------------------------------------------------------------- 1 | td { 2 | width: auto; 3 | } 4 | 5 | th { 6 | white-space: nowrap; 7 | width: 1%; 8 | } -------------------------------------------------------------------------------- /src/Frontend/Sorting/SortedTablePage.razor: -------------------------------------------------------------------------------- 1 | @typeparam TableItem -------------------------------------------------------------------------------- /src/Frontend/Sorting/SortedTablePage.razor.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using BlazorTable; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.JSInterop; 5 | using PrimeView.Frontend.Parameters; 6 | using System.Threading.Tasks; 7 | 8 | namespace PrimeView.Frontend.Sorting 9 | { 10 | public partial class SortedTablePage : ComponentBase 11 | { 12 | [Inject] 13 | public NavigationManager NavigationManager { get; set; } 14 | 15 | [Inject] 16 | public ISyncLocalStorageService LocalStorage { get; set; } 17 | 18 | [Inject] 19 | public IJSInProcessRuntime JSRuntime { get; set; } 20 | 21 | [QueryStringParameter("sc")] 22 | public string SortColumn { get; set; } = string.Empty; 23 | 24 | [QueryStringParameter("sd")] 25 | public bool SortDescending { get; set; } = false; 26 | 27 | protected Table sortedTable; 28 | 29 | private bool processTableSortingChange = false; 30 | 31 | public override Task SetParametersAsync(ParameterView parameters) 32 | { 33 | this.SetParametersFromQueryString(NavigationManager, LocalStorage); 34 | 35 | return base.SetParametersAsync(parameters); 36 | } 37 | 38 | protected override async Task OnAfterRenderAsync(bool firstRender) 39 | { 40 | (string sortColumn, bool sortDescending) = this.sortedTable.GetSortParameterValues(); 41 | 42 | if (!this.processTableSortingChange && (!SortColumn.EqualsIgnoreCaseOrNull(sortColumn) || SortDescending != sortDescending)) 43 | { 44 | if (this.sortedTable.SetSortParameterValues(SortColumn, SortDescending)) 45 | await this.sortedTable.UpdateAsync(); 46 | } 47 | 48 | UpdateQueryString(); 49 | 50 | await base.OnAfterRenderAsync(firstRender); 51 | } 52 | 53 | protected void FlagTableSortingChange() 54 | { 55 | this.processTableSortingChange = true; 56 | } 57 | 58 | protected virtual void OnTableRefreshStart() 59 | { 60 | if (!this.processTableSortingChange) 61 | return; 62 | 63 | (string sortColumn, bool sortDescending) = this.sortedTable.GetSortParameterValues(); 64 | 65 | this.processTableSortingChange = false; 66 | 67 | bool queryStringUpdateRequired = false; 68 | 69 | if (!sortColumn.EqualsIgnoreCaseOrNull(SortColumn)) 70 | { 71 | SortColumn = sortColumn; 72 | queryStringUpdateRequired = true; 73 | } 74 | 75 | if (sortDescending != SortDescending) 76 | { 77 | SortDescending = sortDescending; 78 | queryStringUpdateRequired = true; 79 | } 80 | 81 | if (queryStringUpdateRequired) 82 | UpdateQueryString(); 83 | } 84 | 85 | protected virtual void UpdateQueryString() 86 | { 87 | this.UpdateQueryString(NavigationManager, LocalStorage, JSRuntime); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Frontend/Sorting/SortingExtensions.cs: -------------------------------------------------------------------------------- 1 | using BlazorTable; 2 | using PrimeView.Frontend.Parameters; 3 | using System; 4 | 5 | namespace PrimeView.Frontend.Sorting 6 | { 7 | public static class SortingExtensions 8 | { 9 | public static (string sortColumn, bool sortDescending) GetSortParameterValues(this Table table) 10 | { 11 | if (table == null) 12 | return (null, false); 13 | 14 | foreach (var column in table.Columns) 15 | { 16 | if (column.Field == null) 17 | continue; 18 | 19 | if (column.SortColumn) 20 | return (sortColumn: column.Field.GetPropertyParameterName(), sortDescending: column.SortDescending); 21 | } 22 | 23 | return (null, false); 24 | } 25 | 26 | public static bool SetSortParameterValues(this Table table, string sortColumn, bool sortDescending) 27 | { 28 | if (table == null || sortColumn == null) 29 | return false; 30 | 31 | foreach (var column in table.Columns) 32 | { 33 | if (column.Field == null || !column.Sortable) 34 | continue; 35 | 36 | if (column.Field.GetPropertyParameterName().EqualsIgnoreCaseOrNull(sortColumn)) 37 | { 38 | column.SortColumn = true; 39 | column.SortDescending = sortDescending; 40 | 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | 48 | public static bool EqualsIgnoreCaseOrNull(this string x, string y) 49 | { 50 | return (x == null && y == null) || (x != null && y != null && x.Equals(y, StringComparison.OrdinalIgnoreCase)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Frontend/Tools/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace PrimeView.Frontend.Tools 2 | { 3 | public static class Constants 4 | { 5 | public const string ReportCount = nameof(ReportCount); 6 | public const string ActiveReader = nameof(ActiveReader); 7 | public const string SolutionUrlTemplate = nameof(SolutionUrlTemplate); 8 | public const string Readers = nameof(Readers); 9 | public const string JsonFileReader = nameof(JsonFileReader); 10 | public const string RestAPIReader = nameof(RestAPIReader); 11 | 12 | public const string SinglethreadedTag = "st"; 13 | public const string MultithreadedTag = "mt"; 14 | public const string BaseTag = "ba"; 15 | public const string WheelTag = "wh"; 16 | public const string OtherTag = "ot"; 17 | public const string FaithfulTag = "ff"; 18 | public const string UnfaithfulTag = "uf"; 19 | public const string UnknownTag = "uk"; 20 | public const string OneTag = "on"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Frontend/Tools/ILanguageInfoProvider.cs: -------------------------------------------------------------------------------- 1 | namespace PrimeView.Frontend.Tools 2 | { 3 | public interface ILanguageInfoProvider 4 | { 5 | public LanguageInfo GetLanguageInfo(string language); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Frontend/Tools/LanguageInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace PrimeView.Frontend.Tools 5 | { 6 | public class LanguageInfo 7 | { 8 | public string Key { get; set; } 9 | public string Name { get; set; } 10 | public string URL { get; set; } 11 | public string Tag { get; set; } 12 | 13 | public class KeyEqualityComparer : IEqualityComparer 14 | { 15 | public bool Equals(LanguageInfo x, LanguageInfo y) 16 | { 17 | return (x == null && y == null) || (x != null && y != null && x.Key == y.Key); 18 | } 19 | 20 | public int GetHashCode([DisallowNull] LanguageInfo obj) 21 | { 22 | return obj.Key.GetHashCode(); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Frontend/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using PrimeView.Frontend 10 | @using PrimeView.Frontend.Shared 11 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MaxReportCount": "30", 3 | "ReportCount": "50", 4 | "ActiveReader": "RestAPIReader", 5 | "SolutionUrlTemplate": "https://github.com/PlummersSoftwareLLC/Primes/tree/drag-race/Prime{tag}/solution_{sln}", 6 | "Readers": { 7 | "JsonFileReader": { 8 | "BaseURI": "https://primes.fra1.digitaloceanspaces.com/", 9 | "IsS3Bucket": "true" 10 | }, 11 | "RestAPIReader": { 12 | "APIBaseURI": "" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body, .page { 2 | min-height: 100%; 3 | height: 100%; 4 | } 5 | 6 | .content { 7 | padding-top: 1.1rem; 8 | } 9 | 10 | .valid.modified:not([type=checkbox]) { 11 | outline: 1px solid #26b050; 12 | } 13 | 14 | .invalid { 15 | outline: 1px solid red; 16 | } 17 | 18 | .validation-message { 19 | color: red; 20 | } 21 | 22 | #blazor-error-ui { 23 | background: lightyellow; 24 | bottom: 0; 25 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 26 | display: none; 27 | left: 0; 28 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 29 | position: fixed; 30 | width: 100%; 31 | z-index: 1000; 32 | } 33 | 34 | #blazor-error-ui .dismiss { 35 | cursor: pointer; 36 | position: absolute; 37 | right: 0.75rem; 38 | top: 0.5rem; 39 | } 40 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/data/langmap.json: -------------------------------------------------------------------------------- 1 | { 2 | "ada": { 3 | "name": "Ada", 4 | "url": "https://en.wikipedia.org/wiki/Ada_(programming_language)" 5 | }, 6 | "algol68g": { 7 | "name": "Algol 68G", 8 | "url": "https://en.wikipedia.org/wiki/ALGOL_68" 9 | }, 10 | "amd64": { 11 | "name": "AMD64 machine code", 12 | "url": "https://en.wikipedia.org/wiki/X86-64", 13 | "tag": "Amd64" 14 | }, 15 | "apl": { 16 | "name": "APL", 17 | "url": "https://en.wikipedia.org/wiki/APL_(programming_language)" 18 | }, 19 | "assembly": { 20 | "name": "Assembly", 21 | "url": "https://en.wikipedia.org/wiki/Assembly_language" 22 | }, 23 | "assemblyscript": { 24 | "name": "AssemblyScript", 25 | "url": "https://www.assemblyscript.org/" 26 | }, 27 | "awk": { 28 | "name": "AWK", 29 | "url": "https://en.wikipedia.org/wiki/AWK" 30 | }, 31 | "ballerina": { 32 | "name": "Ballerina", 33 | "url": "https://ballerina.io/" 34 | }, 35 | "bash": { 36 | "name": "Bash", 37 | "url": "https://www.gnu.org/software/bash/" 38 | }, 39 | "basic": { 40 | "name": "BASIC", 41 | "url": "https://en.wikipedia.org/wiki/BASIC" 42 | }, 43 | "beef": { 44 | "name": "Beef", 45 | "url": "https://www.beeflang.org/" 46 | }, 47 | "befunge": { 48 | "name": "Befunge", 49 | "url": "https://en.wikipedia.org/wiki/Befunge" 50 | }, 51 | "boxlang": { 52 | "name": "BoxLang", 53 | "url": "https://www.boxlang.io/", 54 | "tag": "BoxLang" 55 | }, 56 | "brainfuck": { 57 | "name": "Brainfuck", 58 | "url": "https://en.wikipedia.org/wiki/Brainfuck", 59 | "tag": "BrainFuck" 60 | }, 61 | "c": { 62 | "name": "C", 63 | "url": "https://en.wikipedia.org/wiki/C_(programming_language)" 64 | }, 65 | "clipper": { 66 | "name": "Clipper", 67 | "url": "https://en.wikipedia.org/wiki/Clipper_(programming_language)" 68 | }, 69 | "centurionpl": { 70 | "name": "Centurion PL", 71 | "url": "https://github.com/Nakazoto/CenturionComputer/blob/main/Reference/CPU6%20Programmer%20Manual/02_CPL_ocr.pdf", 72 | "tag": "CenturionPL" 73 | }, 74 | "cfml": { 75 | "name": "CFML", 76 | "url": "https://en.wikipedia.org/wiki/ColdFusion_Markup_Language", 77 | "tag": "CFML" 78 | }, 79 | "chapel": { 80 | "name": "Chapel", 81 | "url": "https://chapel-lang.org/" 82 | }, 83 | "clojure": { 84 | "name": "Clojure", 85 | "url": "https://clojure.org/" 86 | }, 87 | "cobol": { 88 | "name": "COBOL", 89 | "url": "https://en.wikipedia.org/wiki/COBOL" 90 | }, 91 | "comal": { 92 | "name": "COMAL", 93 | "url": "https://en.wikipedia.org/wiki/COMAL" 94 | }, 95 | "cpp": { 96 | "name": "C++", 97 | "url": "https://isocpp.org/", 98 | "tag": "CPP" 99 | }, 100 | "crystal": { 101 | "name": "Crystal", 102 | "url": "https://crystal-lang.org/" 103 | }, 104 | "csharp": { 105 | "name": "C#", 106 | "url": "https://docs.microsoft.com/en-us/dotnet/csharp/", 107 | "tag": "CSharp" 108 | }, 109 | "cython": { 110 | "name": "Cython", 111 | "url": "https://cython.org/" 112 | }, 113 | "d": { 114 | "name": "D", 115 | "url": "https://dlang.org/" 116 | }, 117 | "dart": { 118 | "name": "Dart", 119 | "url": "https://dart.dev/" 120 | }, 121 | "delphi": { 122 | "name": "Delphi", 123 | "url": "https://www.embarcadero.com/products/delphi" 124 | }, 125 | "elixir": { 126 | "name": "Elixir", 127 | "url": "https://elixir-lang.org/" 128 | }, 129 | "emojicode": { 130 | "name": "Emojicode", 131 | "url": "https://www.emojicode.org/" 132 | }, 133 | "erlang": { 134 | "name": "Erlang", 135 | "url": "https://www.erlang.org/" 136 | }, 137 | "euphoria": { 138 | "name": "Euphoria", 139 | "url": "https://en.wikipedia.org/wiki/Euphoria_(programming_language)" 140 | }, 141 | "forth": { 142 | "name": "Forth", 143 | "url": "https://en.wikipedia.org/wiki/Forth_(programming_language)" 144 | }, 145 | "fortran": { 146 | "name": "Fortran", 147 | "url": "https://wg5-fortran.org/" 148 | }, 149 | "fsharp": { 150 | "name": "F#", 151 | "url": "https://fsharp.org/", 152 | "tag": "FSharp" 153 | }, 154 | "gdscript": { 155 | "name": "GDScript", 156 | "url": "https://gdscript.com/" 157 | }, 158 | "go": { 159 | "name": "Go", 160 | "url": "https://golang.org/" 161 | }, 162 | "groovy": { 163 | "name": "Groovy", 164 | "url": "https://groovy-lang.org/" 165 | }, 166 | "hack": { 167 | "name": "Hack", 168 | "url": "https://hacklang.org/" 169 | }, 170 | "haskell": { 171 | "name": "Haskell", 172 | "url": "https://www.haskell.org/" 173 | }, 174 | "haxe": { 175 | "name": "Haxe", 176 | "url": "https://haxe.org/" 177 | }, 178 | "idl": { 179 | "name": "IDL", 180 | "url": "https://en.wikipedia.org/wiki/IDL_(programming_language)" 181 | }, 182 | "java": { 183 | "name": "Java", 184 | "url": "https://www.java.com/" 185 | }, 186 | "javascript": { 187 | "name": "JavaScript", 188 | "url": "https://www.javascript.com/" 189 | }, 190 | "julia": { 191 | "name": "Julia", 192 | "url": "https://julialang.org/" 193 | }, 194 | "kermit": { 195 | "name": "Kermit", 196 | "url": "https://www.kermitproject.org/index.html" 197 | }, 198 | "kos": { 199 | "name": "Kos", 200 | "url": "https://github.com/kos-lang/kos" 201 | }, 202 | "kotlin": { 203 | "name": "Kotlin", 204 | "url": "https://kotlinlang.org/" 205 | }, 206 | "latex": { 207 | "name": "LaTeX", 208 | "url": "https://www.latex-project.org/" 209 | }, 210 | "lean4": { 211 | "name": "Lean 4", 212 | "url": "https://leanprover.github.io/", 213 | "tag": "Lean4" 214 | }, 215 | "lisp": { 216 | "name": "Common Lisp", 217 | "url": "https://en.wikipedia.org/wiki/Common_Lisp", 218 | "tag": "Lisp" 219 | }, 220 | "lua": { 221 | "name": "Lua", 222 | "url": "https://www.lua.org/" 223 | }, 224 | "m": { 225 | "name": "M (MUMPS)", 226 | "url": "https://en.wikipedia.org/wiki/MUMPS", 227 | "tag": "M" 228 | }, 229 | "macro11": { 230 | "name": "MACRO-11", 231 | "url": "https://en.wikipedia.org/wiki/MACRO-11", 232 | "tag": "MACRO11" 233 | }, 234 | "mixal": { 235 | "name": "MIXAL", 236 | "url": "https://en.wikipedia.org/wiki/MIX" 237 | }, 238 | "mixed": { 239 | "name": "(mixed)", 240 | "tag": "Mixed" 241 | }, 242 | "nim": { 243 | "name": "Nim", 244 | "url": "https://nim-lang.org/" 245 | }, 246 | "nodejs": { 247 | "name": "Node.js", 248 | "url": "https://nodejs.org/", 249 | "tag": "NodeJS" 250 | }, 251 | "ocaml": { 252 | "name": "OCaml", 253 | "url": "https://ocaml.org/" 254 | }, 255 | "octave": { 256 | "name": "Octave", 257 | "url": "https://www.gnu.org/software/octave/" 258 | }, 259 | "odin": { 260 | "name": "Odin", 261 | "url": "https://odin-lang.org/" 262 | }, 263 | "pascal": { 264 | "name": "Pascal", 265 | "url": "https://en.wikipedia.org/wiki/Pascal_(programming_language)" 266 | }, 267 | "pdl": { 268 | "name": "Perl Data Language", 269 | "url": "http://pdl.perl.org/", 270 | "tag": "PDL" 271 | }, 272 | "perl": { 273 | "name": "Perl", 274 | "url": "https://www.perl.org/" 275 | }, 276 | "phix": { 277 | "name": "Phix", 278 | "url": "http://phix.x10.mx/" 279 | }, 280 | "php": { 281 | "name": "PHP", 282 | "url": "https://www.php.net/" 283 | }, 284 | "pony": { 285 | "name": "Pony", 286 | "url": "https://www.ponylang.io/" 287 | }, 288 | "postscript": { 289 | "name": "PostScript", 290 | "url": "https://nl.wikipedia.org/wiki/PostScript" 291 | }, 292 | "powershell": { 293 | "name": "PowerShell", 294 | "url": "https://docs.microsoft.com/en-gb/powershell/" 295 | }, 296 | "prolog": { 297 | "name": "Prolog", 298 | "url": "https://en.wikipedia.org/wiki/Prolog" 299 | }, 300 | "pyret": { 301 | "name": "Pyret", 302 | "url": "https://pyret.org/index.html" 303 | }, 304 | "python": { 305 | "name": "Python", 306 | "url": "https://www.python.org/" 307 | }, 308 | "r": { 309 | "name": "R", 310 | "url": "https://www.r-project.org/" 311 | }, 312 | "raku": { 313 | "name": "Raku", 314 | "url": "https://www.raku.org/" 315 | }, 316 | "red": { 317 | "name": "Red", 318 | "url": "https://www.red-lang.org/" 319 | }, 320 | "rexx": { 321 | "name": "Rexx", 322 | "url": "http://www.rexxinfo.org/", 323 | "tag": "REXX" 324 | }, 325 | "ruby": { 326 | "name": "Ruby", 327 | "url": "https://www.ruby-lang.org/" 328 | }, 329 | "rust": { 330 | "name": "Rust", 331 | "url": "https://www.rust-lang.org/" 332 | }, 333 | "scala": { 334 | "name": "Scala", 335 | "url": "https://www.scala-lang.org/" 336 | }, 337 | "scheme": { 338 | "name": "Scheme", 339 | "url": "https://en.wikipedia.org/wiki/Scheme_(programming_language)" 340 | }, 341 | "smalltalk": { 342 | "name": "Smalltalk", 343 | "url": "https://en.wikipedia.org/wiki/Smalltalk" 344 | }, 345 | "sql": { 346 | "name": "SQL", 347 | "url": "https://en.wikipedia.org/wiki/SQL" 348 | }, 349 | "sqlite": { 350 | "name": "SQLite", 351 | "url": "https://www.sqlite.org/" 352 | }, 353 | "squirrel": { 354 | "name": "Squirrel", 355 | "url": "http://squirrel-lang.org/" 356 | }, 357 | "standardml": { 358 | "name": "Standard ML", 359 | "url": "https://en.wikipedia.org/wiki/Standard_ML", 360 | "tag": "StandardML" 361 | }, 362 | "swift": { 363 | "name": "Swift", 364 | "url": "https://swift.org/" 365 | }, 366 | "tcl": { 367 | "name": "Tcl", 368 | "url": "https://www.tcl.tk/" 369 | }, 370 | "tex": { 371 | "name": "TeX", 372 | "url": "https://en.wikipedia.org/wiki/TeX" 373 | }, 374 | "typescript": { 375 | "name": "TypeScript", 376 | "url": "https://www.typescriptlang.org/" 377 | }, 378 | "umple": { 379 | "name": "Umple", 380 | "url": "https://cruise.umple.org/umple/" 381 | }, 382 | "unicat": { 383 | "name": "Unicat", 384 | "url": "https://esolangs.org/wiki/Unicat" 385 | }, 386 | "v": { 387 | "name": "V", 388 | "url": "https://vlang.io/" 389 | }, 390 | "verilog": { 391 | "name": "Verilog", 392 | "url": "https://en.wikipedia.org/wiki/Verilog" 393 | }, 394 | "whitespace": { 395 | "name": "Whitespace", 396 | "url": "https://en.wikipedia.org/wiki/Whitespace_(programming_language)" 397 | }, 398 | "wren": { 399 | "name": "Wren", 400 | "url": "https://wren.io/" 401 | }, 402 | "yoix": { 403 | "name": "Yoix", 404 | "url": "https://en.wikipedia.org/wiki/Yoix" 405 | }, 406 | "zig": { 407 | "name": "Zig", 408 | "url": "https://ziglang.org/" 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlummersSoftwareLLC/PrimeView/d81dc1f1a4ad73405c216ebadee0184868c80c02/src/Frontend/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Frontend/wwwroot/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 24 | 26 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PrimeView 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
 
35 |
Loading...
36 |
This might take a while...
37 |
38 |
39 |
40 | 41 |
42 | An unhandled error has occurred. 43 | Reload 44 | 🗙 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/js/app.js: -------------------------------------------------------------------------------- 1 | var PrimeViewJS = PrimeViewJS || {}; 2 | 3 | PrimeViewJS.ShowUrl = function (url) { 4 | window.history.replaceState(null, "", url); 5 | }; 6 | 7 | PrimeViewJS.GetMultiselectValues = function (element, valueSeparator) { 8 | var selectedValues = []; 9 | 10 | for (var i = 0; i < element.length; i++) { 11 | if (element.options[i].selected) 12 | selectedValues.push(element.options[i].value); 13 | } 14 | 15 | return selectedValues.length > 0 ? selectedValues.join(valueSeparator) : null; 16 | }; 17 | 18 | PrimeViewJS.ClearMultiselectValues = function (element) { 19 | for (var i = 0; i < element.length; i++) { 20 | if (element.options[i].selected) 21 | element.options[i].selected = false; 22 | } 23 | }; 24 | 25 | PrimeViewJS.SetMultiselectValues = function (element, values) { 26 | for (var i = 0; i < element.length; i++) { 27 | element.options[i].selected = values.includes(element.options[i].value); 28 | } 29 | }; 30 | 31 | PrimeViewJS.DownloadFileFromStream = async function (fileName, mimeType, contentStreamReference) { 32 | const arrayBuffer = await contentStreamReference.arrayBuffer(); 33 | const blob = new Blob([arrayBuffer], { type: mimeType }); 34 | const url = URL.createObjectURL(blob); 35 | const anchorElement = document.createElement('a'); 36 | anchorElement.href = url; 37 | anchorElement.download = fileName ?? ''; 38 | anchorElement.click(); 39 | anchorElement.remove(); 40 | URL.revokeObjectURL(url); 41 | } 42 | 43 | var blinkTexts = []; 44 | var blinkCount = 0; 45 | 46 | function blinkInit () { 47 | if (!$("#blinkTextMessage")) 48 | return false; 49 | 50 | $(".blinkTextContent").each((_index, element) => { 51 | blinkTexts.push($(element).text()); 52 | }); 53 | 54 | return blinkTexts.length > 0; 55 | } 56 | 57 | function blinkText () { 58 | var blinkTextDiv = $("#blinkTextMessage"); 59 | if(!blinkTextDiv) 60 | return; 61 | 62 | if (blinkCount >= blinkTexts.length) 63 | blinkCount = 0; 64 | 65 | blinkTextDiv.html(blinkTexts[blinkCount++]); 66 | blinkTextDiv.fadeIn(300).animate({ opacity: 1.0 }).fadeOut(300, () => blinkText()); 67 | } 68 | 69 | if (blinkInit()) 70 | blinkText(); 71 | -------------------------------------------------------------------------------- /src/Frontend/wwwroot/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationFallback": { 3 | "rewrite": "/index.html", 4 | "exclude": ["/_content/*", "/_framework/*", "/css/*.{css}", "/data/*.{json}", "/img/*", "/js/*.{js}", "/*.{ico,png}", "/appsettings.*"] 5 | }, 6 | "responseOverrides": { 7 | "404": { 8 | "rewrite": "/index.html" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/JsonFileReader/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace PrimeView.JsonFileReader 2 | { 3 | static class Constants 4 | { 5 | public const string BaseURI = nameof(BaseURI); 6 | public const string Index = nameof(Index); 7 | public const string IsS3Bucket = nameof(IsS3Bucket); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/JsonFileReader/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using PrimeView.Entities; 4 | using System; 5 | using System.Text.Json; 6 | 7 | namespace PrimeView.JsonFileReader 8 | { 9 | public static class ExtensionMethods 10 | { 11 | private static readonly JsonSerializerOptions serializerOptions = new() 12 | { 13 | PropertyNameCaseInsensitive = true, 14 | AllowTrailingCommas = true 15 | }; 16 | 17 | public static int GetStableHashCode(this string str) 18 | { 19 | unchecked 20 | { 21 | int hash1 = 5381; 22 | int hash2 = hash1; 23 | 24 | for (int i = 0; i < str.Length && str[i] != '\0'; i += 2) 25 | { 26 | hash1 = ((hash1 << 5) + hash1) ^ str[i]; 27 | if (i == str.Length - 1 || str[i + 1] == '\0') 28 | { 29 | break; 30 | } 31 | 32 | hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; 33 | } 34 | 35 | return hash1 + (hash2 * 1566083941); 36 | } 37 | } 38 | 39 | public static IServiceCollection AddJsonFileReportReader(this IServiceCollection serviceCollection, string baseAddress, IConfiguration configuration) 40 | { 41 | return serviceCollection.AddScoped(sp => new ReportReader(baseAddress, configuration)); 42 | } 43 | 44 | public static T? Get(this JsonElement element) 45 | { 46 | try 47 | { 48 | return JsonSerializer.Deserialize(element.GetRawText(), serializerOptions); 49 | } 50 | catch (Exception ex) 51 | { 52 | Console.WriteLine(ex); 53 | } 54 | 55 | return default; 56 | } 57 | 58 | public static T? Get(this JsonElement element, string propertyName) where T : class 59 | { 60 | return GetElement(element, propertyName)?.Get(); 61 | } 62 | 63 | public static T? Get(this JsonElement? element, string propertyName) where T : class 64 | { 65 | return element.HasValue ? Get(element.Value, propertyName) : null; 66 | } 67 | 68 | public static int? GetInt32(this JsonElement? element, string propertyName) 69 | { 70 | return element.HasValue ? GetInt32(element.Value, propertyName) : null; 71 | } 72 | 73 | public static int? GetInt32(this JsonElement element, string propertyName) 74 | { 75 | var childElement = GetElement(element, propertyName); 76 | 77 | return childElement.HasValue && childElement.Value.TryGetInt32(out int value) ? value : null; 78 | } 79 | 80 | public static long? GetInt64(this JsonElement? element, string propertyName) 81 | { 82 | return element.HasValue ? GetInt64(element.Value, propertyName) : null; 83 | } 84 | 85 | public static long? GetInt64(this JsonElement element, string propertyName) 86 | { 87 | var childElement = GetElement(element, propertyName); 88 | 89 | return childElement.HasValue && childElement.Value.TryGetInt64(out long value) ? value : null; 90 | } 91 | 92 | public static double? GetDouble(this JsonElement? element, string propertyName) 93 | { 94 | return element.HasValue ? GetDouble(element.Value, propertyName) : null; 95 | } 96 | 97 | public static double? GetDouble(this JsonElement element, string propertyName) 98 | { 99 | var childElement = GetElement(element, propertyName); 100 | 101 | return childElement.HasValue && childElement.Value.TryGetDouble(out double value) ? value : null; 102 | } 103 | 104 | public static string? GetString(this JsonElement? element, string propertyName) 105 | { 106 | return element.HasValue ? GetString(element.Value, propertyName) : null; 107 | } 108 | 109 | public static string? GetString(this JsonElement element, string propertyName) 110 | { 111 | return GetElement(element, propertyName)?.GetString(); 112 | } 113 | 114 | public static DateTime? GetDateFromUnixTimeSeconds(this JsonElement? element, string propertyName) 115 | { 116 | return element.HasValue ? GetDateFromUnixTimeSeconds(element.Value, propertyName) : null; 117 | } 118 | 119 | public static DateTime? GetDateFromUnixTimeSeconds(this JsonElement element, string propertyName) 120 | { 121 | int? seconds = GetInt32(element, propertyName); 122 | 123 | return seconds.HasValue ? DateTimeOffset.FromUnixTimeSeconds(seconds.Value).DateTime : null; 124 | } 125 | 126 | public static JsonElement? GetElement(this JsonElement element, string propertyName) 127 | { 128 | return element.TryGetProperty(propertyName, out var childElement) ? childElement : null; 129 | } 130 | 131 | public static JsonElement? GetElement(this JsonElement? element, string propertyName) 132 | { 133 | return element.HasValue ? GetElement(element.Value, propertyName) : null; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/JsonFileReader/JsonFileReader.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | PrimeView.JsonFileReader 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/JsonFileReader/README.md: -------------------------------------------------------------------------------- 1 | # Implementation notes 2 | 3 | * This project provides an HTTP "file" reader implementation of the [`IReportReader`](../Entities/IReportReader.cs) interface. It loads benchmark reports in JSON format from a webserver. The webserver can be configured using the `Readers.JsonFileReader.BaseURI` config setting (normally read from [appsettings.json](../Frontend/wwwroot/appsettings.json)), and otherwise defaults to the webserver the PrimeView app is loaded from. Which files it loads (or attempts to) depends on a number of other settings: 4 | * If the `Readers.JsonFileReader.IsS3Bucket` config setting is set to `"true"`, the aforementioned base URI is loaded and expected to be a `ListBucketResult` XML document that lists the files in the bucket. Those files are then retrieved and parsed as reports. If an error occurs while reading or parsing the bucket contents document, the application defaults to the behaviour mentioned in the third point. If reading the bucket file list is successful but an HTTP error occurs when reading or parsing one of the report files, the file in question is skipped, but reading continues. 5 | * If the `Readers.JsonFileReader.Index` config setting is present, the index file it specifies (which should contain a JSON array of file names) is read first, after which the files included in that file are retrieved. If an error occurs while reading or parsing the index, the application defaults to the behaviour mentioned in the next point. If reading the index is successful but an HTTP error occurs when reading one of the report files in the index list, the file in question is skipped, but reading continues. 6 | * In all other cases, report files are read from the the `data` directory on the webserver. It starts with loading `report1.json`, then loads `report2.json`, and so on, until it receives an HTTP error on the request for a `report.json` file. 7 | * The supported JSON format is the one generated by the benchmark tool in the Primes repository, when the `FORMATTER=json` variable is used, with one optional extension: in the metadata object, a string `"user"` property can be added to indicate the user who generated the report. 8 | * The [`ExtensionMethods`](ExtensionMethods.cs) class includes a `GetStableHashCode` method that provides what it says on the tin. This is used to make sure that hash codes of [`Report`](../Entities/Report.cs) JSON blobs loaded from the `report.json` files remain the same across different builds of the PrimeView solution('s projects). This greatly simplifies testing if the [`Report.ID`](../Entities/Report.cs) field is derived from the report's JSON text, which is a fallback if it is not provided otherwise. 9 | -------------------------------------------------------------------------------- /src/JsonFileReader/ReportReader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using PrimeView.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | 10 | namespace PrimeView.JsonFileReader 11 | { 12 | public class ReportReader : IReportReader 13 | { 14 | private List? summaries; 15 | private Dictionary? reportMap; 16 | private readonly HttpClient httpClient; 17 | private readonly string? indexFileName; 18 | private readonly bool isS3Bucket; 19 | private bool haveJsonFilesLoaded = false; 20 | private bool reachedMaxFileCount = false; 21 | private int totalReports = 0; 22 | 23 | public ReportReader(string baseAddress, IConfiguration configuration) 24 | { 25 | this.httpClient = new HttpClient { BaseAddress = new Uri(configuration.GetValue(Constants.BaseURI, baseAddress) ?? baseAddress) }; 26 | this.indexFileName = configuration.GetValue(Constants.Index, null); 27 | this.isS3Bucket = configuration.GetValue(Constants.IsS3Bucket, false); 28 | } 29 | 30 | private async Task LoadReportJsonFile(string fileName) 31 | { 32 | if (string.IsNullOrEmpty(fileName)) 33 | return null; 34 | 35 | if (haveJsonFilesLoaded && reportMap!.TryGetValue(fileName, out Report? report)) 36 | return report; 37 | 38 | string reportJson; 39 | 40 | try 41 | { 42 | reportJson = await this.httpClient.GetStringAsync(fileName); 43 | } 44 | catch 45 | { 46 | return null; 47 | } 48 | 49 | var reportElement = JsonDocument.Parse(reportJson).RootElement; 50 | return ParseReportElement(reportJson, reportElement, fileName); 51 | } 52 | 53 | private async Task LoadReportJsonFiles(int maxFileCount) 54 | { 55 | if (this.haveJsonFilesLoaded && (!this.reachedMaxFileCount || maxFileCount <= this.reportMap!.Count)) 56 | return; 57 | 58 | string[]? reportFileNames = null; 59 | 60 | if (this.isS3Bucket) 61 | reportFileNames = await S3BucketIndexReader.GetFileNames(this.httpClient); 62 | 63 | else if (this.indexFileName != null) 64 | { 65 | try 66 | { 67 | reportFileNames = JsonSerializer.Deserialize(await this.httpClient.GetStringAsync(this.indexFileName)); 68 | } 69 | catch { } 70 | } 71 | 72 | this.summaries = []; 73 | this.reportMap = []; 74 | this.reachedMaxFileCount = false; 75 | this.totalReports = reportFileNames?.Length ?? maxFileCount; 76 | 77 | Dictionary> stringReaderMap = []; 78 | 79 | for (int fileIndex = 0; fileIndex != reportFileNames?.Length; fileIndex++) 80 | { 81 | string fileName = reportFileNames != null ? reportFileNames[fileIndex] : $"data/report{fileIndex + 1}.json"; 82 | 83 | stringReaderMap[fileName] = this.httpClient.GetStringAsync(fileName); 84 | 85 | if (--maxFileCount <= 0) 86 | { 87 | this.reachedMaxFileCount = true; 88 | break; 89 | } 90 | } 91 | 92 | foreach (var item in stringReaderMap) 93 | { 94 | string reportJson; 95 | string fileName = item.Key; 96 | 97 | try 98 | { 99 | reportJson = await item.Value; 100 | } 101 | catch (HttpRequestException) 102 | { 103 | // break out of the loop on error if we're not reading a list of files we retrieved from the index file... 104 | if (reportFileNames == null) 105 | break; 106 | 107 | // ...otherwise try reading the next file 108 | else 109 | continue; 110 | } 111 | 112 | try 113 | { 114 | var reportElement = JsonDocument.Parse(reportJson).RootElement; 115 | Report report = ParseReportElement(reportJson, reportElement, fileName); 116 | 117 | this.reportMap[fileName] = report; 118 | this.summaries.Add(ExtractSummary(report)); 119 | } 120 | catch 121 | { 122 | Console.WriteLine($"Report parsing of file {fileName} failed"); 123 | } 124 | } 125 | 126 | this.haveJsonFilesLoaded = true; 127 | } 128 | 129 | private static ReportSummary ExtractSummary(Report report) 130 | { 131 | return new() 132 | { 133 | Id = report.Id, 134 | Architecture = report.OperatingSystem?.Architecture, 135 | CpuBrand = report.CPU?.Brand, 136 | CpuCores = report.CPU?.Cores, 137 | CpuProcessors = report.CPU?.Processors, 138 | CpuVendor = report.CPU?.Vendor, 139 | Date = report.Date, 140 | DockerArchitecture = report.DockerInfo?.Architecture, 141 | IsSystemVirtual = report.System?.IsVirtual, 142 | OsPlatform = report.OperatingSystem?.Platform, 143 | OsDistro = report.OperatingSystem?.Distribution, 144 | OsRelease = report.OperatingSystem?.Release, 145 | ResultCount = report.Results?.Length ?? 0, 146 | User = report.User 147 | }; 148 | } 149 | 150 | private static Report ParseReportElement(string json, JsonElement element, string? id = null) 151 | { 152 | var machineElement = element.GetElement("machine"); 153 | var metadataElement = element.GetElement("metadata"); 154 | 155 | Report report = new() 156 | { 157 | Id = metadataElement.GetString("id") ?? id ?? json.GetStableHashCode().ToString(), 158 | Date = metadataElement.GetDateFromUnixTimeSeconds("date"), 159 | User = metadataElement.GetString("user"), 160 | CPU = machineElement.Get("cpu"), 161 | OperatingSystem = machineElement.Get("os"), 162 | System = machineElement.Get("system"), 163 | DockerInfo = machineElement.Get("docker") 164 | }; 165 | 166 | var resultsElement = element.GetElement("results"); 167 | 168 | List results = []; 169 | 170 | if (resultsElement.HasValue && resultsElement.Value.ValueKind == JsonValueKind.Array) 171 | { 172 | foreach (var resultElement in resultsElement.Value.EnumerateArray()) 173 | { 174 | results.Add(ParseResultElement(resultElement)); 175 | } 176 | } 177 | 178 | report.Results = [.. results]; 179 | 180 | return report; 181 | } 182 | 183 | private static Result ParseResultElement(JsonElement element) 184 | { 185 | var tagsElement = element.GetElement("tags"); 186 | 187 | Result result = new() 188 | { 189 | Algorithm = tagsElement.HasValue ? tagsElement.GetString("algorithm") : "other", 190 | Duration = element.GetDouble("duration"), 191 | Language = element.GetString("implementation"), 192 | IsFaithful = tagsElement.HasValue && tagsElement.GetString("faithful")?.ToLower() == "yes", 193 | Label = element.GetString("label"), 194 | Passes = element.GetInt64("passes"), 195 | Solution = element.GetString("solution"), 196 | Threads = element.GetInt32("threads"), 197 | Status = element.GetString("status") 198 | }; 199 | 200 | if (tagsElement.HasValue && int.TryParse(tagsElement.Value.GetString("bits"), out int bits)) 201 | { 202 | result.Bits = bits; 203 | } 204 | 205 | return result; 206 | } 207 | 208 | public async Task GetReport(string id) 209 | { 210 | return await LoadReportJsonFile(id) ?? new Report(); 211 | } 212 | 213 | public async Task<(ReportSummary[] summaries, int total)> GetSummaries(int maxSummaryCount) 214 | { 215 | return await GetSummaries(null, 0, maxSummaryCount); 216 | } 217 | 218 | public async Task<(ReportSummary[] summaries, int total)> GetSummaries(string? runnerId, int skipFirst, int maxSummaryCount) 219 | { 220 | await LoadReportJsonFiles(skipFirst + maxSummaryCount); 221 | 222 | return (this.summaries!.Skip(skipFirst).Take(maxSummaryCount).ToArray(), totalReports); 223 | } 224 | 225 | public void FlushCache() 226 | { 227 | this.totalReports = 0; 228 | this.haveJsonFilesLoaded = false; 229 | this.summaries = null; 230 | this.reportMap = null; 231 | this.reachedMaxFileCount = false; 232 | } 233 | 234 | public Task GetRunners() 235 | { 236 | return Task.FromResult(Array.Empty()); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/JsonFileReader/S3BucketIndexReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using System.Xml; 7 | using System.Xml.Linq; 8 | 9 | namespace PrimeView.JsonFileReader 10 | { 11 | static class S3BucketIndexReader 12 | { 13 | public static async Task GetFileNames(HttpClient httpClient) 14 | { 15 | XElement indexElement; 16 | 17 | try 18 | { 19 | indexElement = XElement.Parse(await httpClient.GetStringAsync("")); 20 | } 21 | catch 22 | { 23 | return null; 24 | } 25 | 26 | XNamespace ns = indexElement.GetDefaultNamespace(); 27 | 28 | return indexElement.Descendants(ns + "Contents")? 29 | .Select(element => new KeyValuePair(element.Element(ns + "Key")!.Value, XmlConvert.ToDateTime(element.Element(ns + "LastModified")!.Value, XmlDateTimeSerializationMode.Utc))) 30 | .OrderByDescending(pair => pair.Value) 31 | .Select(pair => pair.Key) 32 | .ToArray(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/RestAPIReader/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace PrimeView.RestAPIReader 2 | { 3 | static class Constants 4 | { 5 | public const string APIBaseURI = nameof(APIBaseURI); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/RestAPIReader/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using PrimeView.Entities; 4 | 5 | namespace PrimeView.RestAPIReader 6 | { 7 | public static class ExtensionMethods 8 | { 9 | public static IServiceCollection AddRestAPIReportReader(this IServiceCollection serviceCollection, IConfiguration configuration) 10 | { 11 | return serviceCollection.AddScoped(sp => new ReportReader(configuration)); 12 | } 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RestAPIReader/OpenAPIs/v1.json: -------------------------------------------------------------------------------- 1 | {"components":{"schemas":{"DefaultResponse":{"properties":{"errors":{"items":{"properties":{"message":{"type":"string"},"path":{"type":"string"}},"required":["message"],"type":"object"},"type":"array"}},"required":["errors"],"type":"object"},"Report":{"properties":{"machine":{"properties":{"cpu":{"$ref":"#\/components\/schemas\/ReportMachineCPU"},"docker":{"$ref":"#\/components\/schemas\/ReportMachineDocker"},"os":{"$ref":"#\/components\/schemas\/ReportMachineOS"},"system":{"$ref":"#\/components\/schemas\/ReportMachineSystem"}},"required":["cpu","os","system","docker"],"type":"object"},"metadata":{"$ref":"#\/components\/schemas\/ReportMetadata"},"results":{"items":{"$ref":"#\/components\/schemas\/ReportResult"},"type":"array"},"version":{"type":"string"}},"required":["version","metadata","machine","results"],"type":"object"},"ReportMachineCPU":{"properties":{"brand":{"type":"string"},"cache":{"properties":{"l1d":{"example":"","nullable":true,"type":"string"},"l1i":{"example":"","nullable":true,"type":"string"},"l2":{"example":"","nullable":true,"type":"string"},"l3":{"example":"","nullable":true,"type":"string"}},"required":["l1d","l1i","l2","l3"],"type":"object"},"cores":{"type":"integer"},"efficiencyCores":{"nullable":true,"type":"integer"},"family":{"type":"string"},"flags":{"type":"string"},"governor":{"type":"string"},"manufacturer":{"type":"string"},"model":{"type":"string"},"performanceCores":{"nullable":true,"type":"integer"},"physicalCores":{"type":"integer"},"processors":{"type":"integer"},"revision":{"type":"string"},"socket":{"type":"string"},"speed":{"type":"number"},"speedMax":{"nullable":true,"type":"number"},"speedMin":{"nullable":true,"type":"number"},"stepping":{"type":"string"},"vendor":{"type":"string"},"virtualization":{"type":"boolean"},"voltage":{"type":"string"}},"required":["manufacturer","brand","vendor","family","model","stepping","revision","voltage","speed","governor","cores","physicalCores","processors","socket","flags","virtualization","cache"],"type":"object"},"ReportMachineDocker":{"properties":{"architecture":{"type":"string"},"kernelVersion":{"type":"string"},"memTotal":{"type":"number"},"ncpu":{"type":"integer"},"operatingSystem":{"type":"string"},"osType":{"type":"string"},"osVersion":{"type":"string"},"serverVersion":{"type":"string"}},"required":["kernelVersion","operatingSystem","osVersion","osType","architecture","ncpu","memTotal","serverVersion"],"type":"object"},"ReportMachineOS":{"properties":{"arch":{"type":"string"},"build":{"type":"string"},"codename":{"type":"string"},"codepage":{"type":"string"},"distro":{"type":"string"},"hypervizor":{"nullable":true,"type":"boolean"},"kernel":{"type":"string"},"logofile":{"type":"string"},"platform":{"type":"string"},"release":{"type":"string"},"remoteSession":{"nullable":true,"type":"boolean"},"servicepack":{"type":"string"},"uefi":{"type":"boolean"}},"required":["platform","distro","release","codename","kernel","arch","codepage","logofile","build","servicepack","uefi"],"type":"object"},"ReportMachineSystem":{"properties":{"manufacturer":{"type":"string"},"model":{"type":"string"},"raspberry":{"nullable":true,"properties":{"manufacturer":{"type":"string"},"processor":{"type":"string"},"revision":{"type":"string"},"type":{"type":"string"}},"required":["manufacturer","processor","type","revision"],"type":"object"},"sku":{"type":"string"},"version":{"type":"string"},"virtual":{"type":"boolean"},"virtualHost":{"nullable":true,"type":"string"}},"required":["manufacturer","model","version","sku","virtual"],"type":"object"},"ReportMetadata":{"properties":{"date":{"type":"integer"},"user":{"type":"string"}},"required":["date","user"],"type":"object"},"ReportResult":{"properties":{"duration":{"type":"number"},"implementation":{"type":"string"},"label":{"type":"string"},"passes":{"type":"number"},"solution":{"type":"string"},"tags":{"properties":{"algorithm":{"nullable":true,"type":"string"},"bits":{"nullable":true,"type":"string"},"faithful":{"nullable":true,"type":"string"}},"required":["algorithm","faithful"],"type":"object"},"threads":{"type":"integer"}},"required":["implementation","solution","label","passes","duration","threads","tags"],"type":"object"},"Result":{"properties":{"algorithm":{"type":"string"},"bits":{"nullable":true,"type":"string"},"created_at":{"format":"date-time","type":"string"},"duration":{"type":"number"},"faithful":{"type":"boolean"},"id":{"type":"integer"},"implementation":{"type":"string"},"label":{"type":"string"},"passes":{"type":"number"},"pps":{"type":"number"},"solution":{"type":"string"},"threads":{"type":"integer"}},"required":["id","implementation","solution","label","passes","duration","threads","pps","algorithm","faithful","bits","created_at"],"type":"object"},"Runner":{"properties":{"cpu_brand":{"description":"CPU brand","type":"string"},"cpu_cache_l1d":{"description":"L1 data cache size","nullable":true,"type":"number"},"cpu_cache_l1i":{"description":"L1 instruction cache size","nullable":true,"type":"number"},"cpu_cache_l2":{"description":"L2 cache size","nullable":true,"type":"number"},"cpu_cache_l3":{"description":"L3 cache size","nullable":true,"type":"number"},"cpu_cores":{"description":"CPU core count","type":"number"},"cpu_efficiency_cores":{"description":"Efficiency CPU core count","nullable":true,"type":"number"},"cpu_family":{"description":"CPU family","type":"string"},"cpu_flags":{"description":"CPU flags","type":"string"},"cpu_governor":{"description":"CPU governor","type":"string"},"cpu_manufacturer":{"description":"CPU manufacturer","type":"string"},"cpu_model":{"description":"CPU model","type":"string"},"cpu_performance_cores":{"description":"Performance CPU core count","nullable":true,"type":"number"},"cpu_physical_cores":{"description":"Physical CPU core count","type":"number"},"cpu_processors":{"description":"Processor (CPU) count","type":"number"},"cpu_revision":{"description":"CPU revision","type":"string"},"cpu_socket":{"description":"CPU socket","type":"string"},"cpu_speed":{"description":"CPU speed","type":"number"},"cpu_speed_max":{"description":"Maximum CPU speed","nullable":true,"type":"number"},"cpu_speed_min":{"description":"Minimum CPU speed","nullable":true,"type":"number"},"cpu_stepping":{"description":"CPU stepping","type":"string"},"cpu_vendor":{"description":"CPU vendor","type":"string"},"cpu_virtualization":{"description":"CPU virtualization","type":"boolean"},"cpu_voltage":{"description":"CPU voltage","type":"string"},"docker_architecture":{"description":"Docker architecture","type":"string"},"docker_kernel_version":{"description":"Docker kernel version","type":"string"},"docker_mem_total":{"description":"Docker total memory","type":"number"},"docker_ncpu":{"description":"Docker CPU count","type":"number"},"docker_operating_system":{"description":"Docker OS","type":"string"},"docker_os_type":{"description":"Docker OS type","type":"string"},"docker_os_version":{"description":"Docker OS version","type":"string"},"docker_server_version":{"description":"Docker server version","type":"string"},"id":{"description":"Runner ID","type":"string"},"name":{"description":"Runner name","type":"string"},"os_arch":{"description":"OS architecture","type":"string"},"os_build":{"description":"OS build","type":"string"},"os_codename":{"description":"OS code name","type":"string"},"os_codepage":{"description":"OS code page","type":"string"},"os_distro":{"description":"OS distribution","type":"string"},"os_hypervizor":{"description":"OS hypervizor mode","nullable":true,"type":"boolean"},"os_kernel":{"description":"OS kernel","type":"string"},"os_logofile":{"description":"OS logo file","type":"string"},"os_platform":{"description":"OS platform","type":"string"},"os_release":{"description":"OS release","type":"string"},"os_remote_session":{"description":"OS remote session","nullable":true,"type":"boolean"},"os_servicepack":{"description":"OS service pack","type":"string"},"os_uefi":{"description":"OS UEFI mode","type":"boolean"},"system_manufacturer":{"description":"System manufacturer","type":"string"},"system_model":{"description":"System model","type":"string"},"system_raspberry_manufacturer":{"description":"Raspberry manufacturer","nullable":true,"type":"string"},"system_raspberry_processor":{"description":"Raspberry processor","nullable":true,"type":"string"},"system_raspberry_revision":{"description":"Raspberry revision","nullable":true,"type":"string"},"system_raspberry_type":{"description":"Raspberry type","nullable":true,"type":"string"},"system_sku":{"description":"System SKU","type":"string"},"system_version":{"description":"System version","type":"string"},"system_virtual":{"description":"System is virtual","type":"boolean"},"system_virtual_host":{"description":"System virtual host","nullable":true,"type":"string"}},"required":["id","name","cpu_manufacturer","cpu_brand","cpu_vendor","cpu_family","cpu_model","cpu_stepping","cpu_revision","cpu_voltage","cpu_speed","cpu_speed_min","cpu_speed_max","cpu_governor","cpu_cores","cpu_physical_cores","cpu_efficiency_cores","cpu_performance_cores","cpu_processors","cpu_socket","cpu_flags","cpu_virtualization","cpu_cache_l1d","cpu_cache_l1i","cpu_cache_l2","cpu_cache_l3","os_platform","os_distro","os_release","os_codename","os_kernel","os_arch","os_codepage","os_logofile","os_build","os_servicepack","os_uefi","os_hypervizor","os_remote_session","system_manufacturer","system_model","system_version","system_sku","system_virtual","system_virtual_host","system_raspberry_manufacturer","system_raspberry_processor","system_raspberry_type","system_raspberry_revision","docker_kernel_version","docker_operating_system","docker_os_version","docker_os_type","docker_architecture","docker_ncpu","docker_mem_total","docker_server_version"],"type":"object"},"Runners":{"properties":{"data":{"items":{"$ref":"#\/components\/schemas\/Runner"},"type":"array"},"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"Session":{"properties":{"checksum":{"type":"string"},"created_at":{"description":"Session create date","format":"date-time","type":"string"},"id":{"description":"Session ID","type":"string"},"results":{"items":{"$ref":"#\/components\/schemas\/Result"},"type":"array"},"results_count":{"description":"Number of results generated in the session","type":"number"},"runner":{"$ref":"#\/components\/schemas\/Runner"}},"required":["id","checksum","results_count","runner","created_at"],"type":"object"},"Sessions":{"properties":{"data":{"items":{"$ref":"#\/components\/schemas\/Session"},"type":"array"},"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}},"securitySchemes":{"AuthToken":{"in":"header","name":"X-Token","type":"apiKey"}}},"info":{"contact":{},"description":"API for accesing\/storing results","title":"Primes API","version":"0.1.0"},"openapi":"3.0.0","paths":{"\/reports":{"post":{"operationId":"createReport","requestBody":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Report"}}},"required":true},"responses":{"201":{"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"409":{"description":"Conflict"},"500":{"description":"Failed"}},"security":[{"AuthToken":[]}],"summary":"Create report","tags":["reports"],"x-mojo-to":"Reports#create"}},"\/runners":{"get":{"operationId":"getRunners","parameters":[{"description":"The number of items to skip before starting to collect the result set","in":"query","name":"offset","schema":{"default":"0","minimum":0,"type":"integer"}},{"description":"The numbers of items to return","in":"query","name":"limit","schema":{"default":"20","maximum":100,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Runners"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"List runners","tags":["runners"],"x-mojo-to":"Runners#list"}},"\/runners\/{id}":{"get":{"operationId":"getRunner","parameters":[{"description":"The runner ID","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Runner"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"404":{"description":"Not found"},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"Retrieve runner","tags":["runners"],"x-mojo-to":"Runners#retrieve"}},"\/runners\/{id}\/sessions":{"get":{"operationId":"getRunnerSessions","parameters":[{"description":"The runner ID","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"The number of items to skip before starting to collect the result set","in":"query","name":"offset","schema":{"default":"0","minimum":0,"type":"integer"}},{"description":"The numbers of items to return","in":"query","name":"limit","schema":{"default":"20","maximum":100,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Sessions"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"404":{"description":"Not found"},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"Get sessions from runner","tags":["runners"],"x-mojo-to":"Runners#sessions"}},"\/sessions":{"get":{"operationId":"getSessions","parameters":[{"description":"The number of items to skip before starting to collect the result set","in":"query","name":"offset","schema":{"default":"0","minimum":0,"type":"integer"},"style":"form"},{"description":"The numbers of items to return","in":"query","name":"limit","schema":{"default":"20","maximum":100,"minimum":1,"type":"integer"},"style":"form"},{"description":"Date filter","in":"query","name":"date","schema":{"format":"date","nullable":true,"type":"string"}}],"responses":{"200":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Sessions"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"List sessions","tags":["sessions"],"x-mojo-to":"Sessions#list"}},"\/sessions\/contributions":{"get":{"operationId":"getSessionContributions","parameters":[{"description":"Start date","in":"query","name":"start","required":true,"schema":{"format":"date","type":"string"}},{"description":"End date","in":"query","name":"end","required":true,"schema":{"format":"date","type":"string"}}],"responses":{"200":{"content":{"application\/json":{"schema":{"items":{"properties":{"count":{"type":"number"},"date":{"format":"date","type":"string"}},"required":["date","count"]},"type":"array"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"List daily contributions count","tags":["sessions"],"x-mojo-to":"Sessions#list_contributions"}},"\/sessions\/{id}":{"get":{"operationId":"getSession","parameters":[{"description":"The session ID","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Session"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"404":{"description":"Not found"},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"Retrieve session","tags":["sessions"],"x-mojo-to":"Sessions#retrieve"}},"\/sessions\/{id}\/results":{"get":{"operationId":"getSessionResults","parameters":[{"description":"The session ID","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"},"style":"simple"}],"responses":{"200":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Session"}}},"description":"Success"},"400":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."},"404":{"description":"Not found"},"500":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DefaultResponse"}}},"description":"Default response."}},"summary":"Get results for session","tags":["sessions"],"x-mojo-to":"Sessions#list_results"}}},"servers":[{"url":"https:\/\/primes.marghidanu.com\/v1"}]} -------------------------------------------------------------------------------- /src/RestAPIReader/README.md: -------------------------------------------------------------------------------- 1 | # Implementation notes 2 | 3 | * This project provides a REST API reader implementation of the [`IReportReader`](../Entities/IReportReader.cs) interface. It loads benchmark reports from a REST API that's been developed and published for this purpose. 4 | * The reader lazily loads summaries (`Sessions` in the context of the API) as requested by its invoker. 5 | * The API client is generated from the OpenAPI specification that is published by and for the API. 6 | * The reader uses one configuration setting, which specifies the base URI of the API. 7 | -------------------------------------------------------------------------------- /src/RestAPIReader/ReportReader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using PrimeView.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | 9 | namespace PrimeView.RestAPIReader 10 | { 11 | public class ReportReader : IReportReader 12 | { 13 | private readonly Dictionary> summaryMap = []; 14 | private readonly Dictionary reportMap = []; 15 | private readonly Service.PrimesAPI primesAPI; 16 | private readonly Dictionary totalReportsMap = []; 17 | 18 | public ReportReader(IConfiguration configuration) 19 | { 20 | this.primesAPI = new(new HttpClient()); 21 | if (!string.IsNullOrEmpty(configuration[Constants.APIBaseURI])) 22 | this.primesAPI.BaseUrl = configuration.GetValue(Constants.APIBaseURI); 23 | } 24 | 25 | private async Task<(SortedList summaries, int totalReports)> LoadMissingSummaries(string? runnerId, int skipFirst, int maxSummaryCount) 26 | { 27 | runnerId ??= string.Empty; 28 | 29 | if (!this.summaryMap.ContainsKey(runnerId)) 30 | this.summaryMap.Add(runnerId, []); 31 | 32 | var summaries = summaryMap[runnerId]; 33 | 34 | for (int missingIndex = skipFirst; missingIndex < skipFirst + maxSummaryCount; missingIndex++) 35 | { 36 | // find gaps in the requested key space, and fill them 37 | if (!summaries.ContainsKey(missingIndex)) 38 | { 39 | int missingCount = 0; 40 | 41 | // count number of missing keys, but stop when we've reached the end of the key space we were asked to load 42 | while (!summaries.ContainsKey(missingIndex + ++missingCount) && (missingIndex + missingCount) < (skipFirst + maxSummaryCount)) ; 43 | 44 | await LoadSummaries(summaries, runnerId, missingIndex, missingCount); 45 | 46 | // we may not actually have been able to load all requested missing summaries, but for the sake of filling gaps 47 | // for which data is available in an efficient manner, we'll act like we did; it just means some gaps may remain 48 | missingIndex += missingCount; 49 | } 50 | } 51 | 52 | return (summaries, this.totalReportsMap[runnerId]); 53 | } 54 | 55 | private async Task LoadSummaries(SortedList summaries, string runnerId, int skipFirst, int maxSummaryCount) 56 | { 57 | Service.Sessions sessionsResult; 58 | try 59 | { 60 | if (string.IsNullOrWhiteSpace(runnerId) || !int.TryParse(runnerId, out int parsedRunnerId)) 61 | sessionsResult = await this.primesAPI.GetSessionsAsync(skipFirst, maxSummaryCount, null); 62 | else 63 | sessionsResult = await this.primesAPI.GetRunnerSessionsAsync(parsedRunnerId, skipFirst, maxSummaryCount); 64 | } 65 | catch (Service.ApiException e) 66 | { 67 | Console.Error.WriteLine(e); 68 | return; 69 | } 70 | 71 | int i = 0; 72 | foreach (var session in sessionsResult.Data) 73 | { 74 | var runner = session.Runner; 75 | 76 | ReportSummary summary = new() 77 | { 78 | Id = session.Id, 79 | Date = session.Created_at.DateTime, 80 | User = runner.Name, 81 | Architecture = runner.Os_arch, 82 | CpuBrand = runner.Cpu_brand, 83 | CpuCores = (int)runner.Cpu_cores, 84 | CpuProcessors = (int)runner.Cpu_processors, 85 | CpuVendor = runner.Cpu_vendor, 86 | DockerArchitecture = runner.Docker_architecture, 87 | IsSystemVirtual = runner.System_virtual, 88 | OsPlatform = runner.Os_platform, 89 | OsDistro = runner.Os_distro, 90 | OsRelease = runner.Os_release, 91 | ResultCount = (int)session.Results_count 92 | }; 93 | 94 | summaries.Add(skipFirst + i++, summary); 95 | } 96 | 97 | this.totalReportsMap[runnerId] = sessionsResult.Total; 98 | } 99 | 100 | public async Task GetReport(string id) 101 | { 102 | if (reportMap.TryGetValue(id, out Report? value)) 103 | return value; 104 | 105 | Service.Session? sessionResponse; 106 | try 107 | { 108 | sessionResponse = await this.primesAPI.GetSessionResultsAsync(int.Parse(id)); 109 | } 110 | catch (Service.ApiException e) 111 | { 112 | Console.Error.WriteLine(e); 113 | return new Report(); 114 | } 115 | 116 | (CPUInfo cpu, SystemInfo system, OperatingSystemInfo operatingSystem, DockerInfo dockerInfo) = ParseRunner(sessionResponse.Runner); 117 | 118 | List results = []; 119 | foreach (var apiResult in sessionResponse.Results) 120 | { 121 | Result result = new() 122 | { 123 | Algorithm = apiResult.Algorithm, 124 | Duration = apiResult.Duration, 125 | IsFaithful = apiResult.Faithful, 126 | Label = apiResult.Label, 127 | Language = apiResult.Implementation, 128 | Passes = (long)apiResult.Passes, 129 | Solution = apiResult.Solution, 130 | Threads = apiResult.Threads 131 | }; 132 | 133 | if (int.TryParse(apiResult.Bits, out int bits)) 134 | result.Bits = bits; 135 | 136 | results.Add(result); 137 | } 138 | 139 | Report report = new() 140 | { 141 | Id = sessionResponse.Id, 142 | Date = sessionResponse.Created_at.DateTime, 143 | User = sessionResponse.Runner.Name, 144 | CPU = cpu, 145 | System = system, 146 | OperatingSystem = operatingSystem, 147 | DockerInfo = dockerInfo, 148 | Results = [.. results] 149 | }; 150 | 151 | this.reportMap[id] = report; 152 | 153 | return report; 154 | } 155 | 156 | public async Task GetRunners() 157 | { 158 | Service.Runners runnersResponse; 159 | 160 | try 161 | { 162 | runnersResponse = await this.primesAPI.GetRunnersAsync(0, 100); 163 | } 164 | catch (Service.ApiException e) 165 | { 166 | Console.Error.WriteLine(e); 167 | return []; 168 | } 169 | 170 | List runners = []; 171 | 172 | foreach (var runner in runnersResponse.Data) 173 | { 174 | (CPUInfo cpu, SystemInfo system, OperatingSystemInfo operatingSystem, DockerInfo dockerInfo) = ParseRunner(runner); 175 | 176 | runners.Add(new() 177 | { 178 | Id = runner.Id, 179 | User = runner.Name, 180 | CPU = cpu, 181 | System = system, 182 | OperatingSystem = operatingSystem, 183 | DockerInfo = dockerInfo 184 | }); 185 | } 186 | 187 | return [.. runners]; 188 | } 189 | 190 | private (CPUInfo cpu, SystemInfo system, OperatingSystemInfo operatingSystem, DockerInfo dockerInfo) ParseRunner(Service.Runner runner) 191 | { 192 | CPUInfo cpu = new() 193 | { 194 | Brand = runner.Cpu_brand, 195 | Cores = (int)runner.Cpu_cores, 196 | EfficiencyCores = (int?)runner.Cpu_efficiency_cores, 197 | Family = runner.Cpu_family, 198 | Flags = runner.Cpu_flags, 199 | Governor = runner.Cpu_governor, 200 | Manufacturer = runner.Cpu_manufacturer, 201 | MaximumSpeed = (float?)runner.Cpu_speed_max, 202 | MinimumSpeed = (float?)runner.Cpu_speed_min, 203 | Model = runner.Cpu_model, 204 | PerformanceCores = (int?)runner.Cpu_performance_cores, 205 | PhysicalCores = (int)runner.Cpu_physical_cores, 206 | Processors = (int)runner.Cpu_processors, 207 | RaspberryProcessor = runner.System_raspberry_processor, 208 | Revision = runner.Cpu_revision, 209 | Socket = runner.Cpu_socket, 210 | Speed = (float?)runner.Cpu_speed, 211 | Stepping = runner.Cpu_stepping, 212 | Vendor = runner.Cpu_vendor, 213 | Virtualization = runner.Cpu_virtualization, 214 | Voltage = runner.Cpu_voltage 215 | }; 216 | 217 | Dictionary cache = []; 218 | 219 | if (runner.Cpu_cache_l1d != null) 220 | cache["l1d"] = (long)runner.Cpu_cache_l1d; 221 | if (runner.Cpu_cache_l1i != null) 222 | cache["l1i"] = (long)runner.Cpu_cache_l1i; 223 | if (runner.Cpu_cache_l2 != null) 224 | cache["l2"] = (long)runner.Cpu_cache_l2; 225 | if (runner.Cpu_cache_l3 != null) 226 | cache["l3"] = (long)runner.Cpu_cache_l3; 227 | 228 | if (cache.Count > 0) 229 | cpu.Cache = cache; 230 | 231 | SystemInfo system = new() 232 | { 233 | IsVirtual = runner.System_virtual, 234 | Manufacturer = runner.System_manufacturer, 235 | Model = runner.System_model, 236 | RaspberryManufacturer = runner.System_raspberry_manufacturer, 237 | RaspberryRevision = runner.System_raspberry_revision, 238 | RaspberryType = runner.System_raspberry_type, 239 | SKU = runner.System_sku, 240 | Version = runner.System_version 241 | }; 242 | 243 | OperatingSystemInfo operatingSystem = new() 244 | { 245 | Architecture = runner.Os_arch, 246 | Build = runner.Os_build, 247 | CodeName = runner.Os_codename, 248 | CodePage = runner.Os_codepage, 249 | Distribution = runner.Os_distro, 250 | IsUefi = runner.Os_uefi, 251 | Kernel = runner.Os_kernel, 252 | LogoFile = runner.Os_logofile, 253 | Platform = runner.Os_platform, 254 | Release = runner.Os_release, 255 | ServicePack = runner.Os_servicepack 256 | }; 257 | 258 | DockerInfo dockerInfo = new() 259 | { 260 | Architecture = runner.Docker_architecture, 261 | CPUCount = (int)runner.Docker_ncpu, 262 | KernelVersion = runner.Docker_kernel_version, 263 | OperatingSystem = runner.Docker_operating_system, 264 | OSType = runner.Docker_os_type, 265 | OSVersion = runner.Docker_os_version, 266 | ServerVersion = runner.Docker_server_version, 267 | TotalMemory = (long)runner.Docker_mem_total 268 | }; 269 | 270 | return (cpu, system, operatingSystem, dockerInfo); 271 | } 272 | 273 | public async Task<(ReportSummary[] summaries, int total)> GetSummaries(int maxSummaryCount) 274 | { 275 | return await GetSummaries(null, 0, maxSummaryCount); 276 | } 277 | 278 | public async Task<(ReportSummary[] summaries, int total)> GetSummaries(string? runnerId, int skipFirst, int maxSummaryCount) 279 | { 280 | var (summaries, totalReports) = await LoadMissingSummaries(string.IsNullOrWhiteSpace(runnerId) ? null : runnerId, skipFirst, maxSummaryCount); 281 | 282 | return (summaries.SkipWhile(pair => pair.Key < skipFirst).TakeWhile(pair => pair.Key < skipFirst + maxSummaryCount).Select(pair => pair.Value).ToArray(), totalReports); 283 | } 284 | 285 | public void FlushCache() 286 | { 287 | this.summaryMap.Clear(); 288 | this.reportMap.Clear(); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/RestAPIReader/RestAPIReader.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | PrimeView.RestAPIReader 4 | net8.0 5 | disable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | https://primes.marghidanu.com/v1 27 | 28 | 29 | --------------------------------------------------------------------------------