diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go index cba7871e9..b96490f28 100644 --- a/pkg/vdom/vdom_root.go +++ b/pkg/vdom/vdom_root.go @@ -531,87 +531,6 @@ func (r *RootElem) MakeVDom() *VDomElem { return r.makeVDom(r.Root) } -func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem { - var transferElems []VDomTransferElem - textCounter := 0 // Counter for generating unique IDs for #text nodes - - // Helper function to recursively process each VDomElem in preorder - var processElem func(elem VDomElem) string - processElem = func(elem VDomElem) string { - // Handle #text nodes by generating a unique placeholder ID - if elem.Tag == "#text" { - textId := fmt.Sprintf("text-%d", textCounter) - textCounter++ - transferElems = append(transferElems, VDomTransferElem{ - WaveId: textId, - Tag: elem.Tag, - Text: elem.Text, - Props: nil, - Children: nil, - }) - return textId - } - - // Convert children to WaveId references, handling potential #text nodes - childrenIds := make([]string, len(elem.Children)) - for i, child := range elem.Children { - childrenIds[i] = processElem(child) // Children are not roots - } - - // Create the VDomTransferElem for the current element - transferElem := VDomTransferElem{ - WaveId: elem.WaveId, - Tag: elem.Tag, - Props: elem.Props, - Children: childrenIds, - Text: elem.Text, - } - transferElems = append(transferElems, transferElem) - - return elem.WaveId - } - - // Start processing each top-level element, marking them as roots - for _, elem := range elems { - processElem(elem) - } - - return transferElems -} - -func DedupTransferElems(elems []VDomTransferElem) []VDomTransferElem { - seen := make(map[string]int) // maps WaveId to its index in the result slice - var result []VDomTransferElem - - for _, elem := range elems { - if idx, exists := seen[elem.WaveId]; exists { - // Overwrite the previous element with the latest one - result[idx] = elem - } else { - // Add new element and store its index - seen[elem.WaveId] = len(result) - result = append(result, elem) - } - } - - return result -} - -func (beUpdate *VDomBackendUpdate) CreateTransferElems() { - var vdomElems []VDomElem - for idx, reUpdate := range beUpdate.RenderUpdates { - if reUpdate.VDom == nil { - continue - } - vdomElems = append(vdomElems, *reUpdate.VDom) - beUpdate.RenderUpdates[idx].VDomWaveId = reUpdate.VDom.WaveId - beUpdate.RenderUpdates[idx].VDom = nil - } - transferElems := ConvertElemsToTransferElems(vdomElems) - transferElems = DedupTransferElems(transferElems) - beUpdate.TransferElems = transferElems -} - // SplitBackendUpdate splits a large VDomBackendUpdate into multiple smaller updates // The first update contains all the core fields, while subsequent updates only contain // array elements that need to be appended diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go index e31d2f58b..9cd4ae60c 100644 --- a/pkg/vdom/vdomclient/vdomclient.go +++ b/pkg/vdom/vdomclient/vdomclient.go @@ -55,6 +55,9 @@ type Client struct { OverrideUrlHandler http.Handler NewBlockFlag bool SetupFn func() + TransferElemCache map[string][]byte + TextNodeCache map[string]int + TextNodeNextId int } func (c *Client) GetIsDone() bool { @@ -90,11 +93,14 @@ func MakeClient(appOpts AppOpts) *Client { appOpts.NewBlockFlag = "n" } client := &Client{ - Lock: &sync.Mutex{}, - AppOpts: appOpts, - Root: vdom.MakeRoot(), - DoneCh: make(chan struct{}), - UrlHandlerMux: mux.NewRouter(), + Lock: &sync.Mutex{}, + AppOpts: appOpts, + Root: vdom.MakeRoot(), + DoneCh: make(chan struct{}), + UrlHandlerMux: mux.NewRouter(), + TransferElemCache: make(map[string][]byte), + TextNodeCache: make(map[string]int), + TextNodeNextId: 1, Opts: vdom.VDomBackendOpts{ CloseOnCtrlC: appOpts.CloseOnCtrlC, GlobalKeyboardEvents: appOpts.GlobalKeyboardEvents, diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go index d9aabc861..b60bbed51 100644 --- a/pkg/vdom/vdomclient/vdomserverimpl.go +++ b/pkg/vdom/vdomclient/vdomserverimpl.go @@ -75,7 +75,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom } else { update, err = impl.Client.incrementalRender() } - update.CreateTransferElems() + impl.Client.CreateTransferElems(update) if err != nil { respChan <- wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate]{ diff --git a/pkg/vdom/vdomclient/vdomtransfer.go b/pkg/vdom/vdomclient/vdomtransfer.go new file mode 100644 index 000000000..6fe61ff1e --- /dev/null +++ b/pkg/vdom/vdomclient/vdomtransfer.go @@ -0,0 +1,139 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package vdomclient + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + + "github.com/wavetermdev/waveterm/pkg/vdom" +) + +func transferElemsEqual(t1 *vdom.VDomTransferElem, t2 *vdom.VDomTransferElem) bool { + if t1 == nil || t2 == nil { + return false + } + if t1.WaveId != t2.WaveId || t1.Tag != t2.Tag || t1.Text != t2.Text { + return false + } + if len(t1.Children) != len(t2.Children) { + return false + } + for i := range t1.Children { + if t1.Children[i] != t2.Children[i] { + return false + } + } + return true +} + +func (c *Client) ConvertElemsToTransferElems(elems []vdom.VDomElem) []vdom.VDomTransferElem { + var transferElems []vdom.VDomTransferElem + var textCacheHits int + var teCacheHits int + var numTextNodes int + + // Helper function to recursively process each VDomElem in preorder + var processElem func(elem vdom.VDomElem) string + processElem = func(elem vdom.VDomElem) string { + // Handle #text nodes by generating a unique placeholder ID + if elem.Tag == "#text" { + textId := c.TextNodeCache[elem.Text] + if textId == 0 { + textId = c.TextNodeNextId + c.TextNodeNextId++ + c.TextNodeCache[elem.Text] = textId + } else { + textCacheHits++ + } + textIdStr := fmt.Sprintf("text-%d", textId) + transferElems = append(transferElems, vdom.VDomTransferElem{ + WaveId: textIdStr, + Tag: elem.Tag, + Text: elem.Text, + Props: nil, + Children: nil, + }) + return textIdStr + } + + // Convert children to WaveId references, handling potential #text nodes + childrenIds := make([]string, len(elem.Children)) + for i, child := range elem.Children { + childrenIds[i] = processElem(child) // Children are not roots + } + + // Create the VDomTransferElem for the current element + transferElem := vdom.VDomTransferElem{ + WaveId: elem.WaveId, + Tag: elem.Tag, + Props: elem.Props, + Children: childrenIds, + Text: elem.Text, + } + transferElems = append(transferElems, transferElem) + + return elem.WaveId + } + + // Start processing each top-level element, marking them as roots + for _, elem := range elems { + processElem(elem) + } + + for _, te := range transferElems { + if te.Tag == "#text" { + numTextNodes++ + continue + } + if te.WaveId == "" { + continue + } + curTe := c.TransferElemCache[te.WaveId] + teBytes, _ := json.Marshal(te) + if bytes.Equal(curTe, teBytes) { + teCacheHits++ + } else { + c.TransferElemCache[te.WaveId] = teBytes + } + } + + log.Printf("Converted, transferelems: %d/%d, textcache: %d/%d\n", teCacheHits, len(transferElems)-numTextNodes, textCacheHits, numTextNodes) + return transferElems +} + +func (c *Client) DedupTransferElems(elems []vdom.VDomTransferElem) []vdom.VDomTransferElem { + seen := make(map[string]int) // maps WaveId to its index in the result slice + var result []vdom.VDomTransferElem + + for _, elem := range elems { + if idx, exists := seen[elem.WaveId]; exists { + // Overwrite the previous element with the latest one + result[idx] = elem + } else { + // Add new element and store its index + seen[elem.WaveId] = len(result) + result = append(result, elem) + } + } + + return result +} + +func (c *Client) CreateTransferElems(beUpdate *vdom.VDomBackendUpdate) { + var vdomElems []vdom.VDomElem + for idx, reUpdate := range beUpdate.RenderUpdates { + if reUpdate.VDom == nil { + continue + } + vdomElems = append(vdomElems, *reUpdate.VDom) + beUpdate.RenderUpdates[idx].VDomWaveId = reUpdate.VDom.WaveId + beUpdate.RenderUpdates[idx].VDom = nil + } + transferElems := c.ConvertElemsToTransferElems(vdomElems) + transferElems = c.DedupTransferElems(transferElems) + beUpdate.TransferElems = transferElems +}