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  // }