Rerender on every tick

This commit is contained in:
Smaug123
2022-11-01 22:41:19 +00:00
parent 5bd6f23a11
commit 7584f75b09
5 changed files with 344 additions and 227 deletions

View File

@@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p class="startup-text">Starting up...</p>
<h2>Status of each server</h2>
<table class="server-statuses">
</table>

View File

@@ -9,191 +9,35 @@ module App =
let clusterSize = 5
let serverStatuses =
document.querySelector ".server-statuses" :?> Browser.Types.HTMLTableElement
let ui = Ui.initialise document clusterSize
serverStatuses.border <- "1px"
Table.createHeaderRow document [ "Server" ; "Status" ] serverStatuses
let rec fullyRerender<'a> (cluster : Cluster<'a>) (network : Network<'a>) : Promise<unit> =
let prefs = Ui.getUserPrefs ui
let serverStatusNodes =
fun i ->
let node = document.createElement "tr" :?> Browser.Types.HTMLTableRowElement
let child = document.createElement "td" :?> Browser.Types.HTMLTableCellElement
let statusCell = document.createElement "td" :?> Browser.Types.HTMLTableCellElement
child.textContent <- sprintf "%i" i
node.appendChild child |> ignore
node.appendChild statusCell |> ignore
serverStatuses.appendChild node |> ignore
statusCell
|> List.init clusterSize
Ui.freezeState cluster network
|> Async.StartAsPromise
|> fun p ->
p.``then`` (fun clusterState ->
Ui.render
(perform cluster network)
document
ui
{
UserPreferences = prefs
ClusterState = clusterState
}
)
let logArea = document.querySelector ".log-area" :?> Browser.Types.HTMLTableElement
logArea.border <- "1px"
let renderLogArea (cluster : Cluster<'a>) =
logArea.innerText <- ""
let rows =
List.init
cluster.ClusterSize
(fun i ->
let cell = document.createElement "th" :?> Browser.Types.HTMLTableCellElement
cell.textContent <- sprintf "Server %i" i
Table.createRow document [ Some cell ] logArea
)
rows
|> List.iteri (fun i row ->
let i = i * 1<ServerId>
cluster.GetCurrentInternalState i
|> Async.StartAsPromise
|> fun p ->
p.``then`` (fun state ->
for logEntry in state.Log do
let cell = document.createElement "td" :?> Browser.Types.HTMLTableCellElement
match logEntry with
| None -> cell.textContent <- "<none>"
| Some (value, term) -> cell.textContent <- sprintf "%i: %O" term value
row.appendChild cell |> ignore
)
|> ignore
)
let messageQueueArea =
document.querySelector ".button-area" :?> Browser.Types.HTMLTableElement
messageQueueArea.border <- "1px"
let resetButtonArea () =
messageQueueArea.innerText <- ""
messageQueueArea
|> Table.createHeaderRow document (List.init clusterSize (sprintf "Server %i"))
resetButtonArea ()
let setLeaderState (cluster : Cluster<'a>) (id : int<ServerId>) =
let leaderIdBox =
document.querySelector ".leader-state" :?> Browser.Types.HTMLBlockElement
leaderIdBox.innerText <- sprintf "%i" id
let leaderIdTable =
document.querySelector ".leader-state-table" :?> Browser.Types.HTMLTableElement
leaderIdTable.innerText <- ""
leaderIdTable
|> Table.createHeaderRow document ("" :: List.init clusterSize (sprintf "Server %i"))
let leaderStatePromise = cluster.GetCurrentInternalState id |> Async.StartAsPromise
leaderStatePromise.``then`` (fun state ->
match state.LeaderState with
| None -> leaderIdBox.innerText <- sprintf "%i: not a leader" id
| Some state ->
let knownStoredIndices =
state.MatchIndex
|> Seq.mapi (fun target index ->
let target = target * 1<ServerId>
if target = id then "(self)" else sprintf "%i" index
)
|> Seq.toList
|> fun l -> "Log index known to be stored on each node" :: l
|> List.map (fun text ->
let node = document.createElement "div"
node.innerText <- text
Some node
)
Table.createRow document knownStoredIndices leaderIdTable |> ignore
let nextToSend =
state.ToSend
|> Seq.mapi (fun target index ->
let target = target * 1<ServerId>
if target = id then "(self)" else sprintf "%i" index
)
|> Seq.toList
|> fun l -> "Will try next to send this index" :: l
|> List.map (fun text ->
let node = document.createElement "div"
node.innerText <- text
Some node
)
Table.createRow document nextToSend leaderIdTable |> ignore
)
let printClusterState<'a> (cluster : Cluster<'a>) : unit =
for i in 0 .. cluster.ClusterSize - 1 do
let status = cluster.Status (i * 1<ServerId>)
serverStatusNodes.[i].textContent <- status |> string<ServerStatus>
and perform (cluster : Cluster<'a>) (network : Network<'a>) (action : NetworkAction<'a>) : Promise<unit> =
NetworkAction.perform cluster network action
fullyRerender cluster network
let cluster, network = InMemoryCluster.make<byte> clusterSize
let leaderStateButton =
document.querySelector ".leader-select-button" :?> Browser.Types.HTMLButtonElement
let selectedLeaderId =
document.querySelector ".leader-select" :?> Browser.Types.HTMLInputElement
selectedLeaderId.min <- "0"
selectedLeaderId.max <- sprintf "%i" (clusterSize - 1)
selectedLeaderId.defaultValue <- "0"
leaderStateButton.onclick <-
fun _ ->
let id = selectedLeaderId.valueAsNumber |> int |> (fun i -> i * 1<ServerId>)
setLeaderState cluster id
let performWithoutPrintingNetworkState action =
NetworkAction.perform cluster network action
printClusterState cluster
renderLogArea cluster
let rec printNetworkState<'a> (network : Network<'a>) : unit =
resetButtonArea ()
let allButtons =
[ 0 .. network.ClusterSize - 1 ]
|> List.map (fun i ->
let i = i * 1<ServerId>
network.UndeliveredMessages i
|> List.map (fun (messageId, message) ->
Button.create
document
(sprintf "Server %i, message %i: %O" i messageId message)
(fun button ->
button.remove ()
NetworkMessage (i, messageId) |> performWithoutPrintingNetworkState
printNetworkState network
)
)
)
let maxQueueLength = allButtons |> Seq.map List.length |> Seq.max
let allButtons' =
allButtons
|> List.map (fun l -> List.append (List.map Some l) (List.replicate (maxQueueLength - List.length l) None))
|> List.transpose
for row in allButtons' do
Table.createRow document row messageQueueArea |> ignore
let perform (action : NetworkAction<_>) : unit =
performWithoutPrintingNetworkState action
printNetworkState network
let startupText =
document.querySelector ".startup-text" :?> Browser.Types.HTMLParagraphElement
leaderStateButton.onclick <- fun _ -> fullyRerender cluster network
let startupSequence =
[
@@ -226,72 +70,40 @@ module App =
NetworkAction.NetworkMessage (0<ServerId>, 2)
NetworkAction.NetworkMessage (1<ServerId>, 6)
]
|> fun s -> (Constructors.Promise.resolve (printClusterState cluster), s)
||> List.fold (fun (inPromise : Promise<unit>) action -> inPromise.``then`` (fun () -> perform action))
|> fun p -> p.``then`` (fun () -> startupText.textContent <- "")
|> fun s -> (fullyRerender cluster network, s)
||> List.fold (fun (inPromise : Promise<unit>) action ->
promise {
let! _ = inPromise
return! perform cluster network action
}
)
let timeoutButton =
document.querySelector ".timeout-button" :?> Browser.Types.HTMLButtonElement
let timeoutField =
document.querySelector ".timeout-text" :?> Browser.Types.HTMLInputElement
timeoutField.max <- string<int> (clusterSize - 1)
timeoutField.min <- "0"
timeoutField.defaultValue <- "0"
timeoutButton.onclick <-
fun _event ->
startupSequence.``then`` (fun () ->
timeoutField.valueAsNumber
ui.TimeoutField.valueAsNumber
|> int
|> fun i -> i * 1<ServerId>
|> InactivityTimeout
|> perform
printClusterState cluster
renderLogArea cluster
printNetworkState network
|> perform cluster network
)
let heartbeatButton =
document.querySelector ".heartbeat-button" :?> Browser.Types.HTMLButtonElement
let heartbeatField =
document.querySelector ".heartbeat-text" :?> Browser.Types.HTMLInputElement
heartbeatField.max <- string<int> (clusterSize - 1)
heartbeatField.min <- "0"
heartbeatField.defaultValue <- "0"
heartbeatButton.onclick <-
fun _event ->
startupSequence.``then`` (fun () ->
heartbeatField.valueAsNumber
ui.HeartbeatField.valueAsNumber
|> int
|> fun i -> i * 1<ServerId>
|> Heartbeat
|> perform
printClusterState cluster
renderLogArea cluster
printNetworkState network
|> perform cluster network
)
let clientDataField =
document.querySelector ".client-data" :?> Browser.Types.HTMLInputElement
clientDataField.max <- "255"
clientDataField.min <- "0"
clientDataField.defaultValue <- "0"
let clientDataServerField =
document.querySelector ".client-server-selection" :?> Browser.Types.HTMLInputElement
clientDataServerField.max <- string<int> (clusterSize - 1)
clientDataServerField.min <- "0"
clientDataServerField.defaultValue <- "0"
let clientDataSubmitButton =
document.querySelector ".client-data-submit" :?> Browser.Types.HTMLButtonElement
@@ -299,12 +111,10 @@ module App =
fun _event ->
startupSequence.``then`` (fun () ->
let server =
clientDataServerField.valueAsNumber |> int |> (fun i -> i * 1<ServerId>)
ui.ClientDataServerField.valueAsNumber |> int |> (fun i -> i * 1<ServerId>)
let data = clientDataField.valueAsNumber |> byte
NetworkAction.ClientRequest (server, data, printfn "%O") |> perform
let data = ui.ClientDataField.valueAsNumber |> byte
printClusterState cluster
renderLogArea cluster
printNetworkState network
NetworkAction.ClientRequest (server, data, printfn "%O")
|> perform cluster network
)

View File

@@ -3,7 +3,7 @@ namespace RaftFable
[<RequireQualifiedAccess>]
module Button =
let create (document : Browser.Types.Document) (text : string) (onClick : Browser.Types.HTMLButtonElement -> unit) =
let create (document : Browser.Types.Document) (text : string) (onClick : Browser.Types.HTMLButtonElement -> 'a) =
let node = document.createElement "button" :?> Browser.Types.HTMLButtonElement
node.textContent <- text
node.onclick <- fun _ -> onClick node

View File

@@ -5,6 +5,7 @@
<ItemGroup>
<Compile Include="Table.fs" />
<Compile Include="Button.fs" />
<Compile Include="Ui.fs" />
<Compile Include="App.fs" />
</ItemGroup>
<ItemGroup>

307
RaftFable/src/Ui.fs Normal file
View File

@@ -0,0 +1,307 @@
namespace RaftFable
open Raft
type ClusterState<'a> =
{
ClusterSize : int
InternalState : ServerInternalState<'a> array
Statuses : ServerStatus array
UndeliveredMessages : (int * Message<'a>) list array
}
type UserPreferences =
{
LeaderUnderConsideration : int<ServerId>
}
type UiBackingState<'a> =
{
ClusterState : ClusterState<'a>
UserPreferences : UserPreferences
}
type UiElements =
{
Document : Browser.Types.Document
ServerStatusTable : Browser.Types.HTMLTableElement
LogArea : Browser.Types.HTMLTableElement
MessageQueueArea : Browser.Types.HTMLTableElement
LeaderStateTable : Browser.Types.HTMLTableElement
TimeoutField : Browser.Types.HTMLInputElement
ClientDataField : Browser.Types.HTMLInputElement
ClientDataServerField : Browser.Types.HTMLInputElement
HeartbeatField : Browser.Types.HTMLInputElement
SelectedLeaderId : Browser.Types.HTMLInputElement
}
type RequiresPopulation =
{
ServerStatusNodes : Browser.Types.HTMLTableCellElement array
}
[<RequireQualifiedAccess>]
module Ui =
let initialise (document : Browser.Types.Document) (clusterSize : int) : UiElements =
let serverStatuses =
document.querySelector ".server-statuses" :?> Browser.Types.HTMLTableElement
let logArea = document.querySelector ".log-area" :?> Browser.Types.HTMLTableElement
let messageQueueArea =
document.querySelector ".button-area" :?> Browser.Types.HTMLTableElement
let leaderStateTable =
document.querySelector ".leader-state-table" :?> Browser.Types.HTMLTableElement
let timeoutButton =
document.querySelector ".timeout-button" :?> Browser.Types.HTMLButtonElement
let timeoutField =
document.querySelector ".timeout-text" :?> Browser.Types.HTMLInputElement
let clientDataServerField =
document.querySelector ".client-server-selection" :?> Browser.Types.HTMLInputElement
let heartbeatField =
document.querySelector ".heartbeat-text" :?> Browser.Types.HTMLInputElement
let clientDataField =
document.querySelector ".client-data" :?> Browser.Types.HTMLInputElement
let selectedLeaderId =
document.querySelector ".leader-select" :?> Browser.Types.HTMLInputElement
{
Document = document
ServerStatusTable = serverStatuses
LogArea = logArea
MessageQueueArea = messageQueueArea
LeaderStateTable = leaderStateTable
TimeoutField = timeoutField
ClientDataField = clientDataField
ClientDataServerField = clientDataServerField
HeartbeatField = heartbeatField
SelectedLeaderId = selectedLeaderId
}
let reset (clusterSize : int) (ui : UiElements) : RequiresPopulation =
let document = ui.Document
ui.ServerStatusTable.innerText <- ""
ui.ServerStatusTable.border <- "1px"
Table.createHeaderRow ui.Document [ "Server" ; "Status" ] ui.ServerStatusTable
let serverStatusNodes =
[|
for i in 0 .. clusterSize - 1 do
let node = document.createElement "tr" :?> Browser.Types.HTMLTableRowElement
let child = document.createElement "td" :?> Browser.Types.HTMLTableCellElement
let statusCell = document.createElement "td" :?> Browser.Types.HTMLTableCellElement
child.textContent <- sprintf "%i" i
node.appendChild child |> ignore
node.appendChild statusCell |> ignore
ui.ServerStatusTable.appendChild node |> ignore
yield statusCell
|]
ui.SelectedLeaderId.min <- "0"
ui.SelectedLeaderId.max <- sprintf "%i" (clusterSize - 1)
ui.SelectedLeaderId.defaultValue <- "0"
ui.ClientDataField.max <- "255"
ui.ClientDataField.min <- "0"
ui.ClientDataField.defaultValue <- "0"
ui.HeartbeatField.max <- string<int> (clusterSize - 1)
ui.HeartbeatField.min <- "0"
ui.HeartbeatField.defaultValue <- "0"
ui.ClientDataServerField.max <- string<int> (clusterSize - 1)
ui.ClientDataServerField.min <- "0"
ui.ClientDataServerField.defaultValue <- "0"
ui.TimeoutField.max <- string<int> (clusterSize - 1)
ui.TimeoutField.min <- "0"
ui.TimeoutField.defaultValue <- "0"
ui.LeaderStateTable.innerText <- ""
ui.LeaderStateTable
|> Table.createHeaderRow document ("" :: List.init clusterSize (sprintf "Server %i"))
ui.MessageQueueArea.border <- "1px"
ui.MessageQueueArea.innerText <- ""
ui.MessageQueueArea
|> Table.createHeaderRow document (List.init clusterSize (sprintf "Server %i"))
ui.LogArea.border <- "1px"
ui.LogArea.innerText <- ""
{
ServerStatusNodes = serverStatusNodes
}
let render<'a>
(perform : NetworkAction<'a> -> Fable.Core.JS.Promise<unit>)
(document : Browser.Types.Document)
(ui : UiElements)
(state : UiBackingState<'a>)
: unit =
let userPrefs = state.UserPreferences
let state = state.ClusterState
let requiresPopulation = reset state.ClusterSize ui
let rows =
List.init
state.ClusterSize
(fun i ->
let cell = document.createElement "th" :?> Browser.Types.HTMLTableCellElement
cell.textContent <- sprintf "Server %i" i
Table.createRow document [ Some cell ] ui.LogArea
)
rows
|> List.iteri (fun i row ->
let state = state.InternalState.[i]
for logEntry in state.Log do
let cell = document.createElement "td" :?> Browser.Types.HTMLTableCellElement
match logEntry with
| None -> cell.textContent <- "<none>"
| Some (value, term) -> cell.textContent <- sprintf "%i: %O" term value
row.appendChild cell |> ignore
)
for i in 0 .. state.ClusterSize - 1 do
let status = state.Statuses.[i]
requiresPopulation.ServerStatusNodes.[i].textContent <- status |> string<ServerStatus>
// Network status
let allButtons =
[ 0 .. state.ClusterSize - 1 ]
|> List.map (fun i ->
state.UndeliveredMessages.[i]
|> List.map (fun (messageId, message) ->
Button.create
document
(sprintf "Server %i, message %i: %O" i messageId message)
(fun button ->
button.remove ()
NetworkMessage (i * 1<ServerId>, messageId) |> perform
)
)
)
let maxQueueLength = allButtons |> Seq.map List.length |> Seq.max
let allButtons' =
allButtons
|> List.map (fun l -> List.append (List.map Some l) (List.replicate (maxQueueLength - List.length l) None))
|> List.transpose
for row in allButtons' do
Table.createRow document row ui.MessageQueueArea |> ignore
// Leader status
let leaderIdBox =
document.querySelector ".leader-state" :?> Browser.Types.HTMLBlockElement
leaderIdBox.innerText <- sprintf "%i" userPrefs.LeaderUnderConsideration
let leaderState =
state.InternalState.[userPrefs.LeaderUnderConsideration / 1<ServerId>]
match leaderState.LeaderState with
| None -> leaderIdBox.innerText <- sprintf "%i: not a leader" userPrefs.LeaderUnderConsideration
| Some leaderState ->
let knownStoredIndices =
leaderState.MatchIndex
|> Seq.mapi (fun target index ->
let target = target * 1<ServerId>
if target = userPrefs.LeaderUnderConsideration then
"(self)"
else
sprintf "%i" index
)
|> Seq.toList
|> fun l -> "Log index known to be stored on each node" :: l
|> List.map (fun text ->
let node = document.createElement "div"
node.innerText <- text
Some node
)
Table.createRow document knownStoredIndices ui.LeaderStateTable |> ignore
let nextToSend =
leaderState.ToSend
|> Seq.mapi (fun target index ->
let target = target * 1<ServerId>
if target = userPrefs.LeaderUnderConsideration then
"(self)"
else
sprintf "%i" index
)
|> Seq.toList
|> fun l -> "Will try next to send this index" :: l
|> List.map (fun text ->
let node = document.createElement "div"
node.innerText <- text
Some node
)
Table.createRow document nextToSend ui.LeaderStateTable |> ignore
let freezeState<'a> (cluster : Cluster<'a>) (network : Network<'a>) : ClusterState<'a> Async =
let internalState =
let states = Array.zeroCreate<ServerInternalState<'a>> cluster.ClusterSize
[ 0 .. cluster.ClusterSize - 1 ]
|> List.map (fun i -> cluster.GetCurrentInternalState (i * 1<ServerId>))
|> List.fold
(fun (i, acc) state ->
(i + 1),
async {
let! acc = acc
let! state = state
states.[i] <- state
return states
}
)
(0, async.Return states)
|> snd
let statuses =
[|
for i in 0 .. cluster.ClusterSize - 1 do
yield cluster.Status (i * 1<ServerId>)
|]
let undeliveredMessages =
[|
for i in 0 .. cluster.ClusterSize - 1 do
yield network.UndeliveredMessages (i * 1<ServerId>)
|]
async {
let! internalState = internalState
return
{
ClusterSize = cluster.ClusterSize
InternalState = internalState
Statuses = statuses
UndeliveredMessages = undeliveredMessages
}
}
let getUserPrefs (ui : UiElements) : UserPreferences =
{
LeaderUnderConsideration = ui.SelectedLeaderId.valueAsNumber |> int |> (fun i -> i * 1<ServerId>)
}