Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
loki/pkg/engine/planner/physical/optimizer.go

187 lines
4.1 KiB

package physical
import (
"slices"
"github.com/grafana/loki/v3/pkg/engine/internal/types"
)
// A rule is a tranformation that can be applied on a Node.
type rule interface {
// apply tries to apply the transformation on the node.
// It returns a boolean indicating whether the transformation has been applied.
apply(Node) bool
}
// removeNoopFilter is a rule that removes Filter nodes without predicates.
type removeNoopFilter struct {
plan *Plan
}
// apply implements rule.
func (r *removeNoopFilter) apply(node Node) bool {
changed := false
switch node := node.(type) {
case *Filter:
if len(node.Predicates) == 0 {
r.plan.eliminateNode(node)
changed = true
}
}
return changed
}
var _ rule = (*removeNoopFilter)(nil)
// predicatePushdown is a rule that moves down filter predicates to the scan nodes.
type predicatePushdown struct {
plan *Plan
}
// apply implements rule.
func (r *predicatePushdown) apply(node Node) bool {
changed := false
switch node := node.(type) {
case *Filter:
for i := 0; i < len(node.Predicates); i++ {
if ok := r.applyPredicatePushdown(node, node.Predicates[i]); ok {
changed = true
// remove predicates that have been pushed down
node.Predicates = slices.Delete(node.Predicates, i, i+1)
i--
}
}
}
return changed
}
func (r *predicatePushdown) applyPredicatePushdown(node Node, predicate Expression) bool {
switch node := node.(type) {
case *DataObjScan:
if canApplyPredicate(predicate) {
node.Predicates = append(node.Predicates, predicate)
return true
}
return false
}
for _, child := range r.plan.Children(node) {
if ok := r.applyPredicatePushdown(child, predicate); !ok {
return ok
}
}
return true
}
func canApplyPredicate(predicate Expression) bool {
switch pred := predicate.(type) {
case *BinaryExpr:
return canApplyPredicate(pred.Left) && canApplyPredicate(pred.Right)
case *ColumnExpr:
return pred.Ref.Type == types.ColumnTypeBuiltin || pred.Ref.Type == types.ColumnTypeMetadata
case *LiteralExpr:
return true
default:
return false
}
}
var _ rule = (*predicatePushdown)(nil)
// limitPushdown is a rule that moves down the limit to the scan nodes.
type limitPushdown struct {
plan *Plan
}
// apply implements rule.
func (r *limitPushdown) apply(node Node) bool {
switch node := node.(type) {
case *Limit:
return r.applyLimitPushdown(node, node.Fetch)
}
return false
}
func (r *limitPushdown) applyLimitPushdown(node Node, limit uint32) bool {
switch node := node.(type) {
case *DataObjScan:
// In case the scan node is reachable from multiple different limit nodes, we need to take the largest limit.
node.Limit = max(node.Limit, limit)
return true
}
for _, child := range r.plan.Children(node) {
if ok := r.applyLimitPushdown(child, limit); !ok {
return ok
}
}
return true
}
var _ rule = (*limitPushdown)(nil)
// optimization represents a single optimization pass and can hold multiple rules.
type optimization struct {
plan *Plan
name string
rules []rule
}
func newOptimization(name string, plan *Plan) *optimization {
return &optimization{
name: name,
plan: plan,
}
}
func (o *optimization) withRules(rules ...rule) *optimization {
o.rules = append(o.rules, rules...)
return o
}
func (o *optimization) optimize(node Node) {
iterations, maxIterations := 0, 3
for iterations < maxIterations {
iterations++
if !o.applyRules(node) {
// Stop immediately if an optimization pass produced no changes.
break
}
}
}
func (o *optimization) applyRules(node Node) bool {
anyChanged := false
for _, child := range o.plan.Children(node) {
changed := o.applyRules(child)
if changed {
anyChanged = true
}
}
for _, rule := range o.rules {
changed := rule.apply(node)
if changed {
anyChanged = true
}
}
return anyChanged
}
// The optimizer can optimize physical plans using the provided optimization passes.
type optimizer struct {
plan *Plan
passes []*optimization
}
func newOptimizer(plan *Plan, passes []*optimization) *optimizer {
return &optimizer{plan: plan, passes: passes}
}
func (o *optimizer) optimize(node Node) {
for _, pass := range o.passes {
pass.optimize(node)
}
}