Skip to content

Commit f6b966d

Browse files
authored
Merge pull request #71 from zaaack/dev
Add multi-thread benchmark for SSR
2 parents d48f7c8 + 588143e commit f6b966d

File tree

9 files changed

+214
-39
lines changed

9 files changed

+214
-39
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
open System
3+
open System.IO
4+
open System.Threading
5+
open System.Threading.Tasks
6+
open System.Diagnostics
7+
open Shared
8+
open Fable.Helpers.ReactServer
9+
open FSharp.Core
10+
let initState: Model = {
11+
counter = Some 42
12+
someString = "Some String"
13+
someFloat = 11.11
14+
someInt = 22
15+
}
16+
17+
let coreCount = Environment.ProcessorCount
18+
let workerTimes = 5000
19+
let totalTimes = workerTimes * coreCount
20+
let mutable totalms = 0L
21+
22+
let reset () =
23+
totalms <- 0L
24+
25+
let render times () =
26+
reset ()
27+
let tid = Thread.CurrentThread.ManagedThreadId
28+
printfn "Thread %i started" tid
29+
let watch = Stopwatch()
30+
watch.Start()
31+
for i = 1 to times do
32+
renderToString(Client.View.view initState ignore) |> ignore
33+
watch.Stop()
34+
totalms <- totalms + watch.ElapsedMilliseconds
35+
printfn "Thread %i render %d times used %dms" tid times watch.ElapsedMilliseconds
36+
int watch.ElapsedMilliseconds
37+
38+
let singleTest () =
39+
let times = workerTimes * 2
40+
let time = render times ()
41+
printfn "[Single thread] %dms %.3freq/s" time ((float times) / (float time) * 1000.)
42+
43+
let tasksTest () =
44+
Tasks.Parallel.For(0, coreCount, (fun _ -> render workerTimes () |> ignore)) |> ignore
45+
Process.GetCurrentProcess().WorkingSet64
46+
47+
let log label memory =
48+
let totalms = totalms / (int64 coreCount)
49+
printfn "[%d %s] Total: %dms Memory footprint: %.3fMB Requests/sec: %.3f" coreCount label totalms ((float memory) / 1024. / 1024.) ((float totalTimes) / (float totalms) * 1000.)
50+
51+
[<EntryPoint>]
52+
let main _ =
53+
reset ()
54+
singleTest ()
55+
reset ()
56+
tasksTest() |> log "tasks"
57+
0
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.0</TargetFramework>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<Reference Include="Shared">
9+
<HintPath>../src/Server/bin/Release/netcoreapp2.0/Server.dll</HintPath>
10+
</Reference>
11+
</ItemGroup>
12+
<ItemGroup>
13+
<Compile Include="./dotnet.fs" />
14+
</ItemGroup>
15+
<Import Project="../.paket/Paket.Restore.targets" />
16+
</Project>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const cluster = require('cluster');
2+
const http = require('http');
3+
const View = require('../src/Client/bin/lib/View')
4+
const ReactDOMServer = require('react-dom/server')
5+
const os = require('os')
6+
const coreCount = os.cpus().length;
7+
8+
const initState = {
9+
counter: 42,
10+
someString: "Some String",
11+
someFloat: 11.11,
12+
someInt: 22,
13+
}
14+
15+
const noop = () => {}
16+
const mb = bytes => (bytes / 1024 / 1024).toFixed(3)
17+
const workerTimes = 5000
18+
const totalTimes = workerTimes * coreCount
19+
20+
function render(len = workerTimes) {
21+
const start = Date.now()
22+
while (len--) {
23+
ReactDOMServer.renderToString(View.view(initState, noop))
24+
}
25+
return Date.now() - start
26+
}
27+
28+
function singleTest() {
29+
const times = workerTimes * 2
30+
const time = render(times)
31+
console.log(`[Single process] ${time}ms ${(times / time * 1000).toFixed(3)}req/s`)
32+
}
33+
34+
if (cluster.isMaster) {
35+
console.log(`Master ${process.pid} is running`);
36+
37+
singleTest()
38+
39+
// Fork workers.
40+
for (let i = 0; i < coreCount; i++) {
41+
const worker = cluster.fork();
42+
}
43+
44+
let totalms = 0
45+
let count = 0
46+
let memoryUsed = 0
47+
function messageHandler(msg) {
48+
if (msg.cmd === 'finished') {
49+
count++
50+
totalms = totalms + msg.time
51+
memoryUsed += msg.memory
52+
if (count >= coreCount) {
53+
totalms = totalms / coreCount
54+
console.log(`[${coreCount} workers] Total: ${totalms}ms Memory footprint: ${mb(memoryUsed)}MB Requests/sec: ${(totalTimes / totalms * 1000).toFixed(3)}`)
55+
for (const id in cluster.workers) {
56+
cluster.workers[id].destroy()
57+
}
58+
process.exit(0)
59+
}
60+
}
61+
}
62+
63+
for (const id in cluster.workers) {
64+
cluster.workers[id].on('message', messageHandler);
65+
}
66+
cluster.on('exit', (worker, code, signal) => {
67+
console.log(`worker ${worker.process.pid} died`);
68+
});
69+
} else {
70+
console.log(`Worker ${process.pid}: started`);
71+
const time = render()
72+
console.log(`Worker ${process.pid}: render ${workerTimes} times used ${time}ms`)
73+
const mem = process.memoryUsage()
74+
process.send({
75+
cmd: 'finished',
76+
time,
77+
memory: mem.heapTotal + mem.external
78+
})
79+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
group Server
2+
FSharp.Core
3+
4+
group Client
5+
Fable.Core
6+
Fable.Import.Browser

Samples/SSRSample/build.fsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ open System
55
open Fake
66

77
let serverPath = "./src/Server" |> FullName
8+
let benchmarkPath = "./benchmark" |> FullName
89
let clientPath = "./src/Client" |> FullName
910
let deployDir = "./deploy" |> FullName
1011

@@ -20,13 +21,18 @@ let yarnTool = platformTool "yarn" "yarn.cmd"
2021
let dotnetcliVersion = DotNetCli.GetDotNetSDKVersionFromGlobalJson()
2122
let mutable dotnetCli = "dotnet"
2223

23-
let run cmd args workingDir =
24+
let runWithEnv cmd args workingDir (env: (string * string) list) =
2425
let result =
2526
ExecProcess (fun info ->
2627
info.FileName <- cmd
28+
for (key, value) in env do
29+
info.Environment.Add(key, value)
2730
info.WorkingDirectory <- workingDir
2831
info.Arguments <- args) TimeSpan.MaxValue
29-
if result <> 0 then failwithf "'%s %s' failed" cmd args
32+
if result <> 0 then failwithf "'%s %s' failedwith" cmd args
33+
34+
let run cmd args workingDir =
35+
runWithEnv cmd args workingDir []
3036

3137
Target "Clean" (fun _ ->
3238
CleanDirs [deployDir]
@@ -54,6 +60,17 @@ Target "Build" (fun () ->
5460
run dotnetCli "fable webpack -- -p" clientPath
5561
)
5662

63+
Target "BuildBench" (fun () ->
64+
run dotnetCli "build --configuration Release" serverPath
65+
run dotnetCli "build --configuration Release" benchmarkPath
66+
run dotnetCli "fable npm-run buildClientLib" clientPath
67+
)
68+
69+
Target "Bench" (fun () ->
70+
run dotnetCli "./bin/Release/netcoreapp2.0/dotnet.dll" benchmarkPath
71+
runWithEnv nodeTool "./node.js" benchmarkPath ["NODE_ENV", "production"]
72+
)
73+
5774
Target "Run" (fun () ->
5875
let server = async {
5976
run dotnetCli "watch run" serverPath
@@ -82,4 +99,8 @@ Target "Run" (fun () ->
8299
==> "RestoreServer"
83100
==> "Run"
84101

102+
103+
"BuildBench"
104+
==> "Bench"
105+
85106
RunTargetOrDefault "Build"

Samples/SSRSample/src/Client/Client.fs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ open Fable.Import.React
1212
open Fable.Import.Browser
1313
open Shared
1414

15-
Client.Bench.jsRenderBench()
16-
17-
1815
// let div = document.getElementById("elmish-app")
1916
// div.innerHTML <- ""
2017
// console.log("root", div)

Samples/SSRSample/src/Client/Client.fsproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
<Compile Include="../../paket-files/fable-elmish/react/src/common.fs" />
1515
<Compile Include="../../paket-files/fable-elmish/react/src/react.fs" />
1616

17-
<Compile Include="Bench.fs" />
1817
<Compile Include="withReactHydrate.fs" />
1918
<Compile Include="Client.fs" />
2019
</ItemGroup>

Samples/SSRSample/src/Server/Server.fs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,6 @@ let initState: Model = {
2727
someFloat = 11.11
2828
someInt = 22
2929
}
30-
31-
let bench () =
32-
let watch = Stopwatch()
33-
watch.Start()
34-
let times = 10000
35-
for i = 1 to times do
36-
Fable.Helpers.ReactServer.renderToString(Client.View.view initState ignore)
37-
|> ignore
38-
watch.Stop()
39-
printfn "render %d times in dotnet core: %dms" times watch.ElapsedMilliseconds
40-
41-
bench ()
42-
4330
let getInitCounter () : Task<Model> = task { return initState }
4431

4532
let htmlTemplate =

docs/server-side-rendering.md

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ There are lots of articles about comparing dotnet core and nodejs, I will only m
3232
* F# is a compiled language, which means it's generally considered faster then a dynamic language, like js.
3333
* Nodejs's single thread, event-driven, non-blocking I/O model works well in most web sites, but it is not good at CPU intensive tasks, including html rendering. Usually we need to run multi nodejs instances to take the advantage of multi-core systems. DotNet support non-blocking I/O (and `async/await` sugar), too. But the awesome part is that it also has pretty good support for multi-thread programming.
3434

35-
In a simple test in my local macbook, rendering on dotnet core is about ~2x faster then nodejs (with ReactDOMServer.renderToString + NODE_ENV=production). You can find more detail in the bottom of this page.
35+
In a simple test in my local macbook, rendering on dotnet core is about ~3x faster then nodejs (with ReactDOMServer.renderToString + NODE_ENV=production). You can find more detail in the bottom of this page.
3636

3737
In a word, with this approach, you can not only get a better performance then nodejs, but also don't need the complexity of running and maintaining nodejs instances on your server!
3838

@@ -257,31 +257,44 @@ cd ./fable-react/Samples/SSRSample/
257257

258258
## Run simple benchmark test in sample app
259259

260-
The result of dotnet core is already printed in console when your server started, here are some commands to run benchmark of ReactDOMServer on nodejs.
260+
The SSRSample project also contains a simple benchmark test, you can try it in you computer by:
261261

262262
```sh
263-
cd ./Samples/SSRSample/src/Client
264-
dotnet fable npm-run buildClientLib
265-
NODE_ENV=production node ./bin/lib/Bench.js
266-
```
267263

268-
### Benchmark result in my laptop (MacBook Pro with 2.7 GHz Intel Core i5, 16 GB 1867 MHz DDR3):
264+
cd ./Samples/SSRSample
265+
./build.sh bench # or ./build.cmd bench on windows
269266

270-
```sh
267+
```
271268

272-
# SSR on dotnet core with Debug mode
273-
# dir: fable-react/Samples/SSRSample/src/Server
274-
$ dotnet run
275-
render 10000 times in dotnet core: 3731ms
269+
Here is the benchmark result in my laptop (MacBook Pro with 2.7 GHz Intel Core i5, 16 GB 1867 MHz DD~R3), rendering on dotnet core is about ~4x faster then on nodejs in a single thread. To take the advantage of multi-core systems, we also tested with multi-thread on dotnet core and cluster mode in nodejs, the dotnet core version is still about ~3x faster then nodejs version, with less memory footprint!
276270

277-
# SSR on dotnet core with Release mode
278-
# dir: fable-react/Samples/SSRSample/src/Server
279-
$ dotnet run --configuration Release
280-
render 10000 times in dotnet core: 2698ms
271+
```sh
281272

282-
# SSR on nodejs with NODE_ENV=production
283-
# dir: fable-react/Samples/SSRSample/src/Client
284-
$ NODE_ENV=production node ./bin/lib/Bench.js
285-
render 10000 times in nodejs: 5782.382ms
273+
dotnet ./bin/Release/netcoreapp2.0/dotnet.dll
274+
Thread 1 started
275+
Thread 1 render 10000 times used 2414ms
276+
[Single thread] 2414ms 4142.502req/s
277+
Thread 1 started
278+
Thread 4 started
279+
Thread 3 started
280+
Thread 5 started
281+
Thread 3 render 5000 times used 3399ms
282+
Thread 5 render 5000 times used 3401ms
283+
Thread 1 render 5000 times used 3402ms
284+
Thread 4 render 5000 times used 3405ms
285+
[4 tasks] Total: 3401ms Memory footprint: 32.184MB Requests/sec: 5880.623
286+
287+
/usr/local/bin/node ./node.js
288+
Master 78856 is running
289+
[Single process] 9511ms 1051.414req/s
290+
Worker 78863: started
291+
Worker 78861: started
292+
Worker 78860: started
293+
Worker 78862: started
294+
Worker 78863: render 5000 times used 10390ms
295+
Worker 78861: render 5000 times used 10459ms
296+
Worker 78862: render 5000 times used 10528ms
297+
Worker 78860: render 5000 times used 10567ms
298+
[4 workers] Total: 10486ms Memory footprint: 104.033MB Requests/sec: 1907.305
286299

287300
```

0 commit comments

Comments
 (0)