github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/build-env.go (about) 1 package vugu 2 3 import ( 4 "encoding/binary" 5 "fmt" 6 7 "github.com/vugu/xxhash" 8 ) 9 10 // NewBuildEnv returns a newly initialized BuildEnv. 11 // The eventEnv is used to implement lifecycle callbacks on components, 12 // it's a vararg for now in order to avoid breaking earlier code but 13 // it should be provided in all new code written. 14 func NewBuildEnv(eventEnv ...EventEnv) (*BuildEnv, error) { 15 // TODO: remove the ... and make it required and check for nil 16 ret := &BuildEnv{} 17 for _, ee := range eventEnv { 18 ret.eventEnv = ee 19 } 20 return ret, nil 21 } 22 23 // BuildEnv is the environment used when building virtual DOM. 24 type BuildEnv struct { 25 26 // wireFunc is called on each componen to inject stuff 27 wireFunc func(c Builder) 28 29 // components in cache pool from prior build 30 compCache map[CompKey]Builder 31 32 // components used so far in this build 33 compUsed map[CompKey]Builder 34 35 // cache of build output by component from prior build pass 36 buildCache map[buildCacheKey]*BuildOut 37 38 // new build output from this build pass (becomes buildCache next build pass) 39 buildResults map[buildCacheKey]*BuildOut 40 41 // lifecycle callbacks need this and it needs to match what the renderer has 42 eventEnv EventEnv 43 44 // track lifecycle callbacks 45 compStateMap map[Builder]compState 46 47 // used to determine "seen in this pass" 48 passNum uint8 49 } 50 51 // BuildResults contains the BuildOut values for full tree of components built. 52 type BuildResults struct { 53 Out *BuildOut 54 55 allOut map[buildCacheKey]*BuildOut 56 } 57 58 // ResultFor is alias for indexing into AllOut. 59 func (r *BuildResults) ResultFor(component interface{}) *BuildOut { 60 return r.allOut[makeBuildCacheKey(component)] 61 } 62 63 // RunBuild performs a bulid on a component, managing the lifecycles of nested components and related concerned. 64 // In the map that is output, m[builder] will give the BuildOut for the component in question. Child components 65 // can likewise be indexed using the component (which should be a struct pointer) as the key. 66 // Callers should not modify the return value as it is reused by subsequent calls. 67 func (e *BuildEnv) RunBuild(builder Builder) *BuildResults { 68 69 if e.compCache == nil { 70 e.compCache = make(map[CompKey]Builder) 71 } 72 if e.compUsed == nil { 73 e.compUsed = make(map[CompKey]Builder) 74 } 75 76 // clear old prior build pass's cache 77 for k := range e.compCache { 78 delete(e.compCache, k) 79 } 80 81 // swap cache and used, so the prior used is the new cache 82 e.compCache, e.compUsed = e.compUsed, e.compCache 83 84 if e.buildCache == nil { 85 e.buildCache = make(map[buildCacheKey]*BuildOut) 86 } 87 if e.buildResults == nil { 88 e.buildResults = make(map[buildCacheKey]*BuildOut) 89 } 90 91 // clear old prior build pass's cache 92 for k := range e.buildCache { 93 delete(e.buildCache, k) 94 } 95 96 // swap cache and results, so the prior results is the new cache 97 e.buildCache, e.buildResults = e.buildResults, e.buildCache 98 99 e.passNum++ 100 101 if e.compStateMap == nil { 102 e.compStateMap = make(map[Builder]compState) 103 } 104 105 var buildIn BuildIn 106 buildIn.BuildEnv = e 107 // buildIn.PositionHashList starts empty 108 109 // recursively build everything 110 e.buildOne(&buildIn, builder) 111 112 // sanity check 113 if len(buildIn.PositionHashList) != 0 { 114 panic(fmt.Errorf("unexpected PositionHashList len = %d", len(buildIn.PositionHashList))) 115 } 116 117 // remove and invoke destroy on anything where passNum doesn't match 118 for k, st := range e.compStateMap { 119 if st.passNum != e.passNum { 120 invokeDestroy(k, e.eventEnv) 121 delete(e.compStateMap, k) 122 } 123 } 124 125 return &BuildResults{allOut: e.buildResults, Out: e.buildResults[makeBuildCacheKey(builder)]} 126 } 127 128 func (e *BuildEnv) buildOne(buildIn *BuildIn, thisb Builder) { 129 130 st, ok := e.compStateMap[thisb] 131 if !ok { 132 invokeInit(thisb, e.eventEnv) 133 } 134 st.passNum = e.passNum 135 e.compStateMap[thisb] = st 136 137 beforeBuilder, ok := thisb.(BeforeBuilder) 138 if ok { 139 beforeBuilder.BeforeBuild() 140 } else { 141 invokeCompute(thisb, e.eventEnv) 142 } 143 144 buildOut := thisb.Build(buildIn) 145 146 // store in buildResults 147 e.buildResults[makeBuildCacheKey(thisb)] = buildOut 148 149 if len(buildOut.Components) == 0 { 150 return 151 } 152 153 // push next position hash to the stack, remove it upon exit 154 nextPositionHash := hashVals(buildIn.CurrentPositionHash()) 155 buildIn.PositionHashList = append(buildIn.PositionHashList, nextPositionHash) 156 defer func() { 157 buildIn.PositionHashList = buildIn.PositionHashList[:len(buildIn.PositionHashList)-1] 158 }() 159 160 for _, c := range buildOut.Components { 161 162 e.buildOne(buildIn, c) 163 164 // each iteration we increment the last position hash (the one we added above) by one 165 buildIn.PositionHashList[len(buildIn.PositionHashList)-1]++ 166 } 167 } 168 169 // CachedComponent will return the component that corresponds to a given CompKey. 170 // The CompKey must contain a unique ID for the instance in question, and an optional 171 // IterKey if applicable in the caller. 172 // A nil value will be returned if nothing is found. During a single build pass 173 // only one component will be returned for a specified key (it is removed from the pool), 174 // in order to protect against 175 // broken callers that accidentally forget to set IterKey properly and ask for the same 176 // component over and over, in whiich case the first call will return a value and 177 // subsequent calls will return nil. 178 func (e *BuildEnv) CachedComponent(compKey CompKey) Builder { 179 ret, ok := e.compCache[compKey] 180 if ok { 181 delete(e.compCache, compKey) 182 return ret 183 } 184 return nil 185 } 186 187 // UseComponent indicates the component which was actually used for a specified CompKey 188 // during this build pass and stores it for later use. In the next build pass, components 189 // which have be provided UseComponent() will be available via CachedComponent(). 190 func (e *BuildEnv) UseComponent(compKey CompKey, component Builder) { 191 delete(e.compCache, compKey) // make sure it's not in the cache 192 e.compUsed[compKey] = component // make sure it is in the used 193 } 194 195 // SetWireFunc assigns the function to be called by WireComponent. 196 // If not set then WireComponent will have no effect. 197 func (e *BuildEnv) SetWireFunc(f func(component Builder)) { 198 e.wireFunc = f 199 } 200 201 // WireComponent calls the wire function on this component. 202 // This is called during component creation and use and provides an 203 // opportunity to inject things into this component. 204 func (e *BuildEnv) WireComponent(component Builder) { 205 if e.wireFunc != nil { 206 e.wireFunc(component) 207 } 208 } 209 210 // hashVals performs a hash of the given values together 211 func hashVals(vs ...uint64) uint64 { 212 h := xxhash.New() 213 var b [8]byte 214 for _, v := range vs { 215 binary.BigEndian.PutUint64(b[:], v) 216 _, err := h.Write(b[:]) 217 if err != nil { 218 panic(err) 219 } 220 } 221 return h.Sum64() 222 223 } 224 225 type compState struct { 226 passNum uint8 227 // TODO: flags? 228 } 229 230 // FIXME: IMPORTANT: If we can separate the hash computation from the equal comparision, then we can use 231 // the hash do map lookups but then have a stable equal comparision, this way components will never 232 // be incorrectly reused, but still get virtually all of the benefits of using the hash approach for 233 // rapid comparision (i.e. "is this probably the same"/"find me one that is probably the same" is fast 234 // to answer). 235 236 // NOTE: seems like we have two very distinct types of component comparisions: 237 // 1. Should we re-use this instance? (Basically, is the input the same - should ignore things like computed properties and other internal state) 238 // ^ This seems like a "shallow" comparision - pointer struct fields should be compared on the basis of do they point to the same thing. 239 // 2. Is this component changed since last render? (This should examine whatever it needs to in order to determine if a re-render is needed) 240 // ^ This seems like a "deep" comparision against a last known rendered state - you don't care about what the pointers are, you 241 // follow it until you get a value, and you check if it's "changed". 242 243 // NOTE: This whole thing seems to be a question of optimization. We could just create 244 // a new component for each pass, but we want to reuse, so it's worth thinking the entire thought through, 245 // and ask what happens if we optmize each step. 246 247 //---- 248 249 /* 250 Points to optimize 251 252 - Don't recreate components that have the same input, reuse them (actually, why?? - because if they 253 compute internal state we sholud preserve that where possible). If we don't do this properly, then the 254 other two optimizations likely won't work either. (THIS IS BASICALLY THE "SHALLOW COMPARE" APPAROACH - BOTH FOR HASHING 255 AND EQUAL COMPARISION - COMPARE THE POINTERS NOT FOLLOWING THEM ETC) 256 257 - Don't re-create VGNode tree for component if output will be the same (algo for how to determine this tbd) 258 - Breaks into "our VGNode stuff is the same" and "ours plus children is the same". 259 260 - Don't re-sync VGNodes to render pipeline if they are the same. 261 - two cases here: 1. Same exact DOM for a component returned , 2. newly generated the same DOM 262 */ 263 264 // NOTE: Should we be using a pool for VGNode and VGAttribute allocation? We are going to be creating and 265 // destroying a whole lot of these. MAYBE, BUT BENCHMARK SHOWS ONLY ABOUT 15% IMPROVEMENT USING THE POOL, MIGHT 266 // BE DIFFERENT IN REAL LIFE BUT PROBABLY NOT WORTH DOING RIGHT OUT THE GATE. 267 268 /* 269 270 Basic sequence: 271 272 - Build is called on root component 273 - Component checks self to see if DOM output will be same as last time and if we have cached BuildOut, return it if so. 274 - No BuildOut cached, run through rest of Build. 275 - For each component encountered, give BuildEnv the populated struct and ask it for the instance to use 276 (it will pull from cache or use the object it was sent). 277 - Component is stored on VGNode.Component field. BuildOut also should keep a slice of these for quick traversal. 278 - BuildOut is returned from root component's Build. 279 - The list of components in the BuildOut is traversed, Build called for each one, 280 and the result set on VGNode.ComponentOut. 281 - This causes the above cycle to run again for each of these child components. Runs until no more are left. 282 - FIXME: need to see how we combine the CSS and JS and make this accessible to the renderer (although maybe 283 the renderer can follow the component trail in BuildOut, or something) 284 285 - At this point we have a BuildOut with a tree of VGNodes, and each one either has content itself or 286 has another BuildOut in the VGNode.ComponentOut field. Between the two caching mechanisms (component 287 checking itself to see if same output, and each component creation checked with BuildEnv for re-use), 288 the cached case for traversing even a large case should be fast. 289 290 - During render: The BuildOut pointer (or maybe its Out field) is used as a cache key - same BuildOut ptr, we assume same 291 VGNodes, and renderer can safely skip to each child component and continue from there. 292 - For each VGNode, we call String() and have a map of the prior output for this position, if it's the same, 293 we can skip all the sync stuff and just move to the next. String() needs to be very carefully implemented 294 so it can be used for equality tests like this safely. The idea is that if we get a different VGNode 295 but with the exact same content, we avoid the extra render instructions. 296 297 ------------------ 298 TODO: We need to verify that component events and slots as planned 299 https://github.com/vugu/vugu/wiki/Component-Related-Features-Design 300 still work with this idea above. I THINK WE CAN JUST ASSIGN THE 301 SLOT AND EVENT CALLBACKS EACH TIME, THAT SHOULD WORK JUST FINE, WE 302 DON'T NEED TO COMPARE AND KEEP THE OLD SLOT FUNCS ETC, JUST OVERWRITE. 303 */ 304 305 /* 306 MORE NOTES: 307 308 On 9/6/19 6:23 PM, Brad Peabody wrote: 309 > each unique position where a component is used could get a unique ID - generated, maybe with type name, doesn't matter really, 310 but then for cases where there is no loop it's just a straight up lookup; in loop cases it's that ID plus a key of some sort 311 (maybe vg-key specifies, with default of index number). could be a struct that has this ID string and key value and that's used 312 to cache which component was used for this last render cycle. 313 > 314 > interesting - then if we know which exact component was in this slot last time, we can just re-assign all of the fields each pass - 315 if they are the same, fine, if not, fine, either way we just assign the fields and tell the component to Build, etc. 316 317 keys can be uint64: uint32 unix timestamp (goes up to 2106-02-07 06:28:15) plus 32 bits of crytographically random data - 318 really should be random enough for all practical purposes (NOW IMPLEMENTED AS CompKey) 319 320 */ 321 322 /* 323 324 STRUCT TAGS: 325 326 type Widget struct { 327 328 // component param 329 Size int `vugu:"cparam"` 330 FirstName *string `vugu:"cparam"` 331 332 // computed property, used for display, but entirely dependent upon Size 333 DisplaySize string 334 } 335 336 */ 337 338 /* 339 340 DIRTY CHECKING: 341 342 Basic idea: 343 344 type DirtyChecker interface{ 345 DirtyCheck(oldData []byte) (isDirty bool, newData []byte) 346 // or maybe just interface{} 347 DirtyCheck(oldData interface{}) (isDirty bool, newData interface{}) 348 } 349 350 // "mod" is good! doesn't sound weird, "modify" is pretty clearly on point, and "mod" is short. 351 352 type ModChecker interface{ 353 ModCheck(oldData interface{}) (isDirty bool, newData interface{}) 354 } 355 356 type SomeComponent struct { 357 FirstName string `vugu:"modcheck"` 358 359 FirstNameFormatted string // computed field, not "modcheck"'ed 360 } 361 362 */ 363 364 // func (e *BuildEnv) Component(vgparent *VGNode, comp Builder) Builder { 365 366 // return comp 367 // } 368 369 // // BuildRoot creates a BuildIn struct and calls Build on the root component (Builder), returning it's output. 370 // func (e *BuildEnv) BuildRoot() (*BuildOut, error) { 371 372 // var buildIn BuildIn 373 // buildIn.BuildEnv = e 374 375 // // TODO: SlotMap? 376 377 // return e.root.Build(&buildIn) 378 // } 379 380 // func (e *BuildEnv) ComponentFor(n *VGNode) (Builder, error) { 381 // panic(fmt.Errorf("not yet implemented")) 382 // } 383 384 // func (e *BuildEnv) SetComponentFor(n *VGNode, c Builder) error { 385 // panic(fmt.Errorf("not yet implemented")) 386 // }