github.com/vugu/vugu@v0.3.5/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 h.Write(b[:]) 217 } 218 return h.Sum64() 219 220 } 221 222 type compState struct { 223 passNum uint8 224 // TODO: flags? 225 } 226 227 // FIXME: IMPORTANT: If we can separate the hash computation from the equal comparision, then we can use 228 // the hash do map lookups but then have a stable equal comparision, this way components will never 229 // be incorrectly reused, but still get virtually all of the benefits of using the hash approach for 230 // rapid comparision (i.e. "is this probably the same"/"find me one that is probably the same" is fast 231 // to answer). 232 233 // NOTE: seems like we have two very distinct types of component comparisions: 234 // 1. Should we re-use this instance? (Basically, is the input the same - should ignore things like computed properties and other internal state) 235 // ^ This seems like a "shallow" comparision - pointer struct fields should be compared on the basis of do they point to the same thing. 236 // 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) 237 // ^ This seems like a "deep" comparision against a last known rendered state - you don't care about what the pointers are, you 238 // follow it until you get a value, and you check if it's "changed". 239 240 // NOTE: This whole thing seems to be a question of optimization. We could just create 241 // a new component for each pass, but we want to reuse, so it's worth thinking the entire thought through, 242 // and ask what happens if we optmize each step. 243 244 //---- 245 246 /* 247 Points to optimize 248 249 - Don't recreate components that have the same input, reuse them (actually, why?? - because if they 250 compute internal state we sholud preserve that where possible). If we don't do this properly, then the 251 other two optimizations likely won't work either. (THIS IS BASICALLY THE "SHALLOW COMPARE" APPAROACH - BOTH FOR HASHING 252 AND EQUAL COMPARISION - COMPARE THE POINTERS NOT FOLLOWING THEM ETC) 253 254 - Don't re-create VGNode tree for component if output will be the same (algo for how to determine this tbd) 255 - Breaks into "our VGNode stuff is the same" and "ours plus children is the same". 256 257 - Don't re-sync VGNodes to render pipeline if they are the same. 258 - two cases here: 1. Same exact DOM for a component returned , 2. newly generated the same DOM 259 */ 260 261 // NOTE: Should we be using a pool for VGNode and VGAttribute allocation? We are going to be creating and 262 // destroying a whole lot of these. MAYBE, BUT BENCHMARK SHOWS ONLY ABOUT 15% IMPROVEMENT USING THE POOL, MIGHT 263 // BE DIFFERENT IN REAL LIFE BUT PROBABLY NOT WORTH DOING RIGHT OUT THE GATE. 264 265 /* 266 267 Basic sequence: 268 269 - Build is called on root component 270 - Component checks self to see if DOM output will be same as last time and if we have cached BuildOut, return it if so. 271 - No BuildOut cached, run through rest of Build. 272 - For each component encountered, give BuildEnv the populated struct and ask it for the instance to use 273 (it will pull from cache or use the object it was sent). 274 - Component is stored on VGNode.Component field. BuildOut also should keep a slice of these for quick traversal. 275 - BuildOut is returned from root component's Build. 276 - The list of components in the BuildOut is traversed, Build called for each one, 277 and the result set on VGNode.ComponentOut. 278 - This causes the above cycle to run again for each of these child components. Runs until no more are left. 279 - FIXME: need to see how we combine the CSS and JS and make this accessible to the renderer (although maybe 280 the renderer can follow the component trail in BuildOut, or something) 281 282 - At this point we have a BuildOut with a tree of VGNodes, and each one either has content itself or 283 has another BuildOut in the VGNode.ComponentOut field. Between the two caching mechanisms (component 284 checking itself to see if same output, and each component creation checked with BuildEnv for re-use), 285 the cached case for traversing even a large case should be fast. 286 287 - During render: The BuildOut pointer (or maybe its Out field) is used as a cache key - same BuildOut ptr, we assume same 288 VGNodes, and renderer can safely skip to each child component and continue from there. 289 - For each VGNode, we call String() and have a map of the prior output for this position, if it's the same, 290 we can skip all the sync stuff and just move to the next. String() needs to be very carefully implemented 291 so it can be used for equality tests like this safely. The idea is that if we get a different VGNode 292 but with the exact same content, we avoid the extra render instructions. 293 294 ------------------ 295 TODO: We need to verify that component events and slots as planned 296 https://github.com/vugu/vugu/wiki/Component-Related-Features-Design 297 still work with this idea above. I THINK WE CAN JUST ASSIGN THE 298 SLOT AND EVENT CALLBACKS EACH TIME, THAT SHOULD WORK JUST FINE, WE 299 DON'T NEED TO COMPARE AND KEEP THE OLD SLOT FUNCS ETC, JUST OVERWRITE. 300 */ 301 302 /* 303 MORE NOTES: 304 305 On 9/6/19 6:23 PM, Brad Peabody wrote: 306 > each unique position where a component is used could get a unique ID - generated, maybe with type name, doesn't matter really, 307 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 308 (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 309 to cache which component was used for this last render cycle. 310 > 311 > 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 - 312 if they are the same, fine, if not, fine, either way we just assign the fields and tell the component to Build, etc. 313 314 keys can be uint64: uint32 unix timestamp (goes up to 2106-02-07 06:28:15) plus 32 bits of crytographically random data - 315 really should be random enough for all practical purposes (NOW IMPLEMENTED AS CompKey) 316 317 */ 318 319 /* 320 321 STRUCT TAGS: 322 323 type Widget struct { 324 325 // component param 326 Size int `vugu:"cparam"` 327 FirstName *string `vugu:"cparam"` 328 329 // computed property, used for display, but entirely dependent upon Size 330 DisplaySize string 331 } 332 333 */ 334 335 /* 336 337 DIRTY CHECKING: 338 339 Basic idea: 340 341 type DirtyChecker interface{ 342 DirtyCheck(oldData []byte) (isDirty bool, newData []byte) 343 // or maybe just interface{} 344 DirtyCheck(oldData interface{}) (isDirty bool, newData interface{}) 345 } 346 347 // "mod" is good! doesn't sound weird, "modify" is pretty clearly on point, and "mod" is short. 348 349 type ModChecker interface{ 350 ModCheck(oldData interface{}) (isDirty bool, newData interface{}) 351 } 352 353 type SomeComponent struct { 354 FirstName string `vugu:"modcheck"` 355 356 FirstNameFormatted string // computed field, not "modcheck"'ed 357 } 358 359 */ 360 361 // func (e *BuildEnv) Component(vgparent *VGNode, comp Builder) Builder { 362 363 // return comp 364 // } 365 366 // // BuildRoot creates a BuildIn struct and calls Build on the root component (Builder), returning it's output. 367 // func (e *BuildEnv) BuildRoot() (*BuildOut, error) { 368 369 // var buildIn BuildIn 370 // buildIn.BuildEnv = e 371 372 // // TODO: SlotMap? 373 374 // return e.root.Build(&buildIn) 375 // } 376 377 // func (e *BuildEnv) ComponentFor(n *VGNode) (Builder, error) { 378 // panic(fmt.Errorf("not yet implemented")) 379 // } 380 381 // func (e *BuildEnv) SetComponentFor(n *VGNode, c Builder) error { 382 // panic(fmt.Errorf("not yet implemented")) 383 // }