slint/internal/compiler/llr/optim_passes/inline_expressions.rs
Simon Hausmann 0d8b0630c7 Avoid inlining @image-url() expressions that can be expensive due to cache misses
If the `image-url()` expression of an `Image { source: @image-url("large-image.png"); ... }` gets inlined into geometry getters and other places that are called for every frame, then we might end up decoding images every frame, if the image isn't in the 5MB image decoder cache. It's better to rely on the `property <image>` of the `Image` for caching the decoded image, so don't inline those.

This fixes CPU being time being spent constantly on decoding images on the home automation lock screen.
2024-11-27 09:10:26 +01:00

211 lines
10 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
//! Inline properties that are simple enough to be inlined
//!
//! If an expression does a single property access or less, it can be inlined
//! in the calling expression
use crate::expression_tree::{BuiltinFunction, ImageReference};
use crate::llr::{CompilationUnit, EvaluationContext, Expression};
const PROPERTY_ACCESS_COST: isize = 1000;
const ALLOC_COST: isize = 700;
const ARRAY_INDEX_COST: isize = 500;
/// The threshold from which we consider an expression to be worth inlining.
/// less than two allocations. (since property access usually cost one allocation)
const INLINE_THRESHOLD: isize = ALLOC_COST * 2 - 10;
/// Property that are used only once should almost always be inlined unless it is really expensive to compute and we want to cache the result
const INLINE_SINGLE_THRESHOLD: isize = ALLOC_COST * 10;
// The cost of an expression.
fn expression_cost(exp: &Expression, ctx: &EvaluationContext) -> isize {
let mut cost = match exp {
Expression::StringLiteral(_) => ALLOC_COST,
Expression::NumberLiteral(_) => 0,
Expression::BoolLiteral(_) => 0,
Expression::PropertyReference(_) => PROPERTY_ACCESS_COST,
Expression::FunctionParameterReference { .. } => return isize::MAX,
Expression::StoreLocalVariable { .. } => 0,
Expression::ReadLocalVariable { .. } => 1,
Expression::StructFieldAccess { .. } => 1,
Expression::ArrayIndex { .. } => ARRAY_INDEX_COST,
Expression::Cast { .. } => 0,
Expression::CodeBlock(_) => 0,
Expression::BuiltinFunctionCall { function, .. } => builtin_function_cost(function),
Expression::CallBackCall { callback, .. } => callback_cost(callback, ctx),
Expression::FunctionCall { function, .. } => callback_cost(function, ctx),
Expression::ExtraBuiltinFunctionCall { .. } => return isize::MAX,
Expression::PropertyAssignment { .. } => return isize::MAX,
Expression::ModelDataAssignment { .. } => return isize::MAX,
Expression::ArrayIndexAssignment { .. } => return isize::MAX,
Expression::BinaryExpression { .. } => 1,
Expression::UnaryOp { .. } => 1,
// Avoid inlining calls to load the image from the cache, as in the worst case the image isn't cached
// and repeated calls will load the image over and over again. It's better to keep the image cached in the
// `property<image>` of the `Image` element, with the exception of embedded textures.
Expression::ImageReference {
resource_ref: ImageReference::EmbeddedTexture { .. }, ..
} => 1,
Expression::ImageReference { .. } => return isize::MAX,
Expression::Condition { condition, true_expr, false_expr } => {
return expression_cost(condition, ctx)
.saturating_add(
expression_cost(true_expr, ctx).max(expression_cost(false_expr, ctx)),
)
.saturating_add(10);
}
// Never inline an array because it is a model and when shared it needs to keep its identity
// (cf #5249) (otherwise it would be `ALLOC_COST`)
Expression::Array { .. } => return isize::MAX,
Expression::Struct { .. } => 1,
Expression::EasingCurve(_) => 1,
Expression::LinearGradient { .. } => ALLOC_COST,
Expression::RadialGradient { .. } => ALLOC_COST,
Expression::EnumerationValue(_) => 0,
Expression::LayoutCacheAccess { .. } => PROPERTY_ACCESS_COST,
Expression::BoxLayoutFunction { .. } => return isize::MAX,
Expression::ComputeDialogLayoutCells { .. } => return isize::MAX,
Expression::MinMax { .. } => 10,
Expression::EmptyComponentFactory => 10,
Expression::TranslationReference { .. } => PROPERTY_ACCESS_COST + 2 * ALLOC_COST,
};
exp.visit(|e| cost = cost.saturating_add(expression_cost(e, ctx)));
cost
}
fn callback_cost(_callback: &crate::llr::PropertyReference, _ctx: &EvaluationContext) -> isize {
// TODO: lookup the callback and find out what it does
isize::MAX
}
fn builtin_function_cost(function: &BuiltinFunction) -> isize {
match function {
BuiltinFunction::GetWindowScaleFactor => PROPERTY_ACCESS_COST,
BuiltinFunction::GetWindowDefaultFontSize => PROPERTY_ACCESS_COST,
BuiltinFunction::AnimationTick => PROPERTY_ACCESS_COST,
BuiltinFunction::Debug => isize::MAX,
BuiltinFunction::Mod => 10,
BuiltinFunction::Round => 10,
BuiltinFunction::Ceil => 10,
BuiltinFunction::Floor => 10,
BuiltinFunction::Abs => 10,
BuiltinFunction::Sqrt => 10,
BuiltinFunction::Cos => 10,
BuiltinFunction::Sin => 10,
BuiltinFunction::Tan => 10,
BuiltinFunction::ACos => 10,
BuiltinFunction::ASin => 10,
BuiltinFunction::ATan => 10,
BuiltinFunction::ATan2 => 10,
BuiltinFunction::Log => 10,
BuiltinFunction::Pow => 10,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX,
BuiltinFunction::ShowPopupWindow
| BuiltinFunction::ClosePopupWindow
| BuiltinFunction::ShowPopupMenu => isize::MAX,
BuiltinFunction::SetSelectionOffsets => isize::MAX,
BuiltinFunction::ItemMemberFunction(..) => isize::MAX,
BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST,
BuiltinFunction::StringToFloat => 50,
BuiltinFunction::StringIsFloat => 50,
BuiltinFunction::ColorRgbaStruct => 50,
BuiltinFunction::ColorHsvaStruct => 50,
BuiltinFunction::ColorBrighter => 50,
BuiltinFunction::ColorDarker => 50,
BuiltinFunction::ColorTransparentize => 50,
BuiltinFunction::ColorMix => 50,
BuiltinFunction::ColorWithAlpha => 50,
BuiltinFunction::ImageSize => 50,
BuiltinFunction::ArrayLength => 50,
BuiltinFunction::Rgb => 50,
BuiltinFunction::Hsv => 50,
BuiltinFunction::ImplicitLayoutInfo(_) => isize::MAX,
BuiltinFunction::ItemAbsolutePosition => isize::MAX,
BuiltinFunction::RegisterCustomFontByPath => isize::MAX,
BuiltinFunction::RegisterCustomFontByMemory => isize::MAX,
BuiltinFunction::RegisterBitmapFont => isize::MAX,
BuiltinFunction::ColorScheme => isize::MAX,
BuiltinFunction::MonthDayCount => isize::MAX,
BuiltinFunction::MonthOffset => isize::MAX,
BuiltinFunction::FormatDate => isize::MAX,
BuiltinFunction::DateNow => isize::MAX,
BuiltinFunction::ValidDate => isize::MAX,
BuiltinFunction::ParseDate => isize::MAX,
BuiltinFunction::SetTextInputFocused => PROPERTY_ACCESS_COST,
BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST,
BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
BuiltinFunction::Use24HourFormat => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
BuiltinFunction::UpdateTimers => 10,
}
}
pub fn inline_simple_expressions(root: &CompilationUnit) {
root.for_each_expression(&mut |e, ctx| {
inline_simple_expressions_in_expression(&mut e.borrow_mut(), ctx)
})
}
fn inline_simple_expressions_in_expression(expr: &mut Expression, ctx: &EvaluationContext) {
if let Expression::PropertyReference(prop) = expr {
let prop_info = ctx.property_info(prop);
if prop_info.analysis.as_ref().is_some_and(|a| !a.is_set && !a.is_set_externally) {
if let Some((binding, map)) = prop_info.binding {
if binding.animation.is_none()
// State info binding are special and the binding cannot be inlined or used.
&& !binding.is_state_info
{
let mapped_ctx = map.map_context(ctx);
let cost = expression_cost(&binding.expression.borrow(), &mapped_ctx);
let use_count = binding.use_count.get();
debug_assert!(
use_count > 0,
"We use a property and its count is zero: {}",
crate::llr::pretty_print::DisplayPropertyRef(prop, ctx)
);
if cost <= INLINE_THRESHOLD
|| (use_count == 1 && cost <= INLINE_SINGLE_THRESHOLD)
{
// Perform inlining
*expr = binding.expression.borrow().clone();
map.map_expression(expr);
// adjust use count
binding.use_count.set(use_count - 1);
if let Some(prop_decl) = prop_info.property_decl {
prop_decl.use_count.set(prop_decl.use_count.get() - 1);
}
adjust_use_count(expr, ctx, 1);
if use_count == 1 {
adjust_use_count(&binding.expression.borrow(), &mapped_ctx, -1);
binding.expression.replace(Expression::CodeBlock(vec![]));
}
}
}
} else if let Some(prop_decl) = prop_info.property_decl {
if let Some(e) = Expression::default_value_for_type(&prop_decl.ty) {
prop_decl.use_count.set(prop_decl.use_count.get() - 1);
*expr = e;
}
}
}
};
expr.visit_mut(|e| inline_simple_expressions_in_expression(e, ctx));
}
fn adjust_use_count(expr: &Expression, ctx: &EvaluationContext, adjust: isize) {
expr.visit_property_references(ctx, &mut |p, ctx| {
let prop_info = ctx.property_info(p);
if let Some(property_decl) = prop_info.property_decl {
property_decl
.use_count
.set(property_decl.use_count.get().checked_add_signed(adjust).unwrap());
}
if let Some((binding, _)) = prop_info.binding {
let use_count = binding.use_count.get().checked_add_signed(adjust).unwrap();
binding.use_count.set(use_count);
}
});
}