chore: avoid allocating intermediate slices (#26320)
This commit is contained in:
parent
109eaeb696
commit
3115bb1a2e
4 changed files with 79 additions and 44 deletions
|
|
@ -1,10 +1,11 @@
|
|||
package planner
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Start returns the earliest slot's start time
|
||||
|
|
@ -87,42 +88,58 @@ func IsFirst(r api.Rate, plan api.Rates) bool {
|
|||
|
||||
// clampRates filters rates to the given time window and adjusts boundary slots
|
||||
func clampRates(rates api.Rates, start, end time.Time) api.Rates {
|
||||
res := make(api.Rates, 0, len(rates)+2)
|
||||
res := make(api.Rates, 0, len(rates))
|
||||
return slices.AppendSeq(res, clampRatesSeq(rates, start, end))
|
||||
}
|
||||
|
||||
for _, r := range rates {
|
||||
// slot before continuous plan
|
||||
if !r.End.After(start) {
|
||||
continue
|
||||
// clampRatesSeq returns an iterator for filtering rates to the given time window and adjusts boundary slots
|
||||
func clampRatesSeq(rates api.Rates, start, end time.Time) iter.Seq[api.Rate] {
|
||||
return func(yield func(api.Rate) bool) {
|
||||
for _, r := range rates {
|
||||
// slot before continuous plan
|
||||
if !r.End.After(start) {
|
||||
continue
|
||||
}
|
||||
|
||||
// slot after continuous plan
|
||||
if !r.Start.Before(end) {
|
||||
continue
|
||||
}
|
||||
|
||||
// calculate adjusted bounds
|
||||
adjustedStart := r.Start
|
||||
if adjustedStart.Before(start) {
|
||||
adjustedStart = start
|
||||
}
|
||||
|
||||
adjustedEnd := r.End
|
||||
if adjustedEnd.After(end) {
|
||||
adjustedEnd = end
|
||||
}
|
||||
|
||||
// skip if adjustment would create invalid slot
|
||||
if !adjustedEnd.After(adjustedStart) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !yield(api.Rate{
|
||||
Start: adjustedStart,
|
||||
End: adjustedEnd,
|
||||
Value: r.Value,
|
||||
}) {
|
||||
return // Stop early if yield returns false
|
||||
}
|
||||
}
|
||||
|
||||
// slot after continuous plan
|
||||
if !r.Start.Before(end) {
|
||||
continue
|
||||
}
|
||||
|
||||
// calculate adjusted bounds
|
||||
adjustedStart := r.Start
|
||||
if adjustedStart.Before(start) {
|
||||
adjustedStart = start
|
||||
}
|
||||
|
||||
adjustedEnd := r.End
|
||||
if adjustedEnd.After(end) {
|
||||
adjustedEnd = end
|
||||
}
|
||||
|
||||
// skip if adjustment would create invalid slot
|
||||
if !adjustedEnd.After(adjustedStart) {
|
||||
continue
|
||||
}
|
||||
|
||||
slot := r
|
||||
slot.Start = adjustedStart
|
||||
slot.End = adjustedEnd
|
||||
res = append(res, slot)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
// SumBySeq sums over a sequence
|
||||
func SumBySeq[T any, R float64](seq iter.Seq[T], iteratee func(item T) R) R {
|
||||
var sum R
|
||||
for t := range seq {
|
||||
sum += iteratee(t)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// findContinuousWindow finds the cheapest continuous window of slots for the given duration.
|
||||
|
|
@ -138,7 +155,7 @@ func findContinuousWindow(rates api.Rates, effectiveDuration time.Duration, targ
|
|||
break
|
||||
}
|
||||
|
||||
cost := lo.SumBy(clampRates(rates[i:], rates[i].Start, windowEnd), func(r api.Rate) float64 {
|
||||
cost := SumBySeq(clampRatesSeq(rates[i:], rates[i].Start, windowEnd), func(r api.Rate) float64 {
|
||||
return float64(r.End.Sub(r.Start)) * r.Value
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import (
|
|||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -93,3 +95,23 @@ func TestSlotAt(t *testing.T) {
|
|||
require.Equal(t, 2.0, SlotAt(now.Add(90*time.Minute), plan).Value)
|
||||
require.True(t, SlotAt(now.Add(3*time.Hour), plan).IsZero())
|
||||
}
|
||||
|
||||
func BenchmarkFindContinuousWindow(b *testing.B) {
|
||||
rr := rates(lo.RepeatBy(96, func(i int) float64 {
|
||||
return float64(i)
|
||||
}), time.Now(), tariff.SlotDuration)
|
||||
|
||||
for b.Loop() {
|
||||
findContinuousWindow(rr, 4*tariff.SlotDuration, rr[len(rr)-1].End)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOptimalPlan(b *testing.B) {
|
||||
rr := rates(lo.RepeatBy(96, func(i int) float64 {
|
||||
return float64(i)
|
||||
}), time.Now(), tariff.SlotDuration)
|
||||
|
||||
for b.Loop() {
|
||||
optimalPlan(rr, 4*tariff.SlotDuration, rr[len(rr)-1].End)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
|
|
@ -35,8 +36,8 @@ func New(log *util.Logger, tariff api.Tariff, opt ...func(t *Planner)) *Planner
|
|||
// It MUST already be established that:
|
||||
// - rates are sorted in ascending order by cost and descending order by start time (prefer late slots)
|
||||
// - rates are filtered to [now, targetTime] window by caller
|
||||
func (t *Planner) plan(rates api.Rates, requiredDuration time.Duration, targetTime time.Time) api.Rates {
|
||||
var plan api.Rates
|
||||
func optimalPlan(rates api.Rates, requiredDuration time.Duration, targetTime time.Time) api.Rates {
|
||||
plan := make(api.Rates, 0, int64(requiredDuration)/int64(tariff.SlotDuration)+3)
|
||||
|
||||
for _, slot := range rates {
|
||||
slotDuration := slot.End.Sub(slot.Start)
|
||||
|
|
@ -199,7 +200,7 @@ func (t *Planner) Plan(requiredDuration, precondition time.Duration, targetTime
|
|||
// sort rates by price and time
|
||||
slices.SortStableFunc(rates, sortByCost)
|
||||
|
||||
plan = t.plan(rates, requiredDuration, targetTime)
|
||||
plan = optimalPlan(rates, requiredDuration, targetTime)
|
||||
|
||||
// sort plan by time
|
||||
plan.Sort()
|
||||
|
|
|
|||
|
|
@ -48,11 +48,6 @@ func TestPlan(t *testing.T) {
|
|||
trf := api.NewMockTariff(ctrl)
|
||||
trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{20, 60, 10, 80, 40, 90}, clock.Now(), time.Hour), nil)
|
||||
|
||||
p := &Planner{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clock,
|
||||
}
|
||||
|
||||
rates, err := trf.Rates()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -60,7 +55,7 @@ func TestPlan(t *testing.T) {
|
|||
|
||||
{
|
||||
// filter rates to [now, now] window - should return empty
|
||||
plan := p.plan(clampRates(rates, clock.Now(), clock.Now()), time.Hour, clock.Now())
|
||||
plan := optimalPlan(clampRates(rates, clock.Now(), clock.Now()), time.Hour, clock.Now())
|
||||
assert.Empty(t, plan)
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +124,7 @@ func TestPlan(t *testing.T) {
|
|||
t.Log(tc.desc)
|
||||
clock.Set(tc.now)
|
||||
// filter rates to [now, target] window as caller would do
|
||||
plan := p.plan(clampRates(rates, tc.now, tc.target), tc.duration, tc.target)
|
||||
plan := optimalPlan(clampRates(rates, tc.now, tc.target), tc.duration, tc.target)
|
||||
|
||||
assert.Equalf(t, tc.planStart.UTC(), Start(plan).UTC(), "case %d start", i)
|
||||
assert.Equalf(t, tc.duration, Duration(plan), "case %d duration", i)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue