@@ -54,8 +54,8 @@ use spar_hir_def::item_tree::ComponentCategory;
5454use crate :: property_accessors:: {
5555 LockingProtocol , get_bottom_half_server, get_critical_section_blocking, get_dispatch_jitter,
5656 get_dispatch_protocol, get_execution_time, get_execution_time_range,
57- get_interrupt_latency_bound, get_isr_execution_time_range , get_isr_priority ,
58- get_locking_protocol, get_processor_binding, get_timing_property,
57+ get_interrupt_latency_bound, get_interrupt_overhead , get_isr_execution_time_range ,
58+ get_isr_priority , get_locking_protocol, get_processor_binding, get_timing_property,
5959} ;
6060use crate :: scheduling_verified:: { self , RtaResult } ;
6161use crate :: { Analysis , AnalysisDiagnostic , Severity , component_path} ;
@@ -130,10 +130,39 @@ impl Analysis for RtaAnalysis {
130130
131131 // ISR execution: prefer ISR_Execution_Time, else
132132 // Compute_Execution_Time. We take the WCET.
133- let ( isr_bcet , isr_wcet ) = get_isr_execution_time_range ( props)
133+ let ( isr_bcet_base , isr_wcet_base ) = get_isr_execution_time_range ( props)
134134 . or_else ( || get_execution_time_range ( props) )
135135 . unwrap_or ( ( 0 , 0 ) ) ;
136136
137+ // v0.9.3 (Tier A #5 cont.): Interrupt_Overhead inflates
138+ // ISR WCET by 1× per firing — vector dispatch, ISR-
139+ // context save/restore, EOI ack — companion to the
140+ // 2× Context_Switch_Time inflation tasks pay per
141+ // dispatch (REQ-RTA-009 / PR #198). The ISR pays this
142+ // cost once per firing, not twice. Default unset = 0
143+ // (byte-identical to v0.9.2). The cost is folded into
144+ // both the per-CPU utilization term and the IRQ-chain
145+ // budget so the kernel-resident interrupt path is
146+ // accounted for everywhere the ISR's WCET is used.
147+ let interrupt_overhead_ps = get_interrupt_overhead ( props) . unwrap_or ( 0 ) ;
148+ let isr_wcet = isr_wcet_base. saturating_add ( interrupt_overhead_ps) ;
149+ let isr_bcet = isr_bcet_base. saturating_add ( interrupt_overhead_ps) ;
150+ if interrupt_overhead_ps > 0 && isr_wcet_base > 0 {
151+ diags. push ( AnalysisDiagnostic {
152+ severity : Severity :: Info ,
153+ message : format ! (
154+ "IsrOverheadInflation: ISR '{}' WCET {} → {} (added 1 × {} \
155+ Interrupt_Overhead per firing)",
156+ comp. name. as_str( ) ,
157+ format_time( isr_wcet_base) ,
158+ format_time( isr_wcet) ,
159+ format_time( interrupt_overhead_ps) ,
160+ ) ,
161+ path : component_path ( instance, idx) ,
162+ analysis : self . name ( ) . to_string ( ) ,
163+ } ) ;
164+ }
165+
137166 // Only admit ISRs that have enough info to contribute
138167 // a utilization term. Otherwise they just exist and
139168 // may still enable chain diagnostics below.
@@ -1577,6 +1606,197 @@ mod tests {
15771606 ) ;
15781607 }
15791608
1609+ // ── O2 (v0.9.3 Tier A #5 cont.): Interrupt_Overhead inflates ISR WCET ─
1610+ #[ test]
1611+ fn interrupt_overhead_inflates_isr_wcet ( ) {
1612+ // With Interrupt_Overhead = 50 us, the ISR's effective WCET is
1613+ // inflated by exactly 50 us per firing (1×, not 2× — contrast
1614+ // with Context_Switch_Time which is applied 2× per task
1615+ // dispatch). Base ISR_Execution_Time = 100 us → effective
1616+ // WCET = 150 us. Diagnostic must mirror OverheadInflation
1617+ // style.
1618+ let ( mut b, root, proc) = make_base ( ) ;
1619+ let dev = add_isr_device ( & mut b, root, "irq_src" , "2 ms" , "100 us" , 99 ) ;
1620+ b. set_property ( dev, "Spar_Timing" , "Interrupt_Overhead" , "50 us" ) ;
1621+ let t1 = b. add_component ( "t1" , ComponentCategory :: Thread , Some ( proc) ) ;
1622+ b. set_children (
1623+ root,
1624+ vec ! [
1625+ ComponentInstanceIdx :: from_raw( la_arena:: RawIdx :: from_u32( 1 ) ) ,
1626+ proc,
1627+ dev,
1628+ ] ,
1629+ ) ;
1630+ b. set_children ( proc, vec ! [ t1] ) ;
1631+ bind_thread ( & mut b, t1, "10 ms" , "1 ms" , Some ( "10 ms" ) ) ;
1632+
1633+ let inst = b. build ( root) ;
1634+ let diags = RtaAnalysis . analyze ( & inst) ;
1635+
1636+ let inflation = diags
1637+ . iter ( )
1638+ . find ( |d| {
1639+ d. severity == Severity :: Info
1640+ && d. message . starts_with ( "IsrOverheadInflation" )
1641+ && d. message . contains ( "'irq_src'" )
1642+ } )
1643+ . unwrap_or_else ( || {
1644+ panic ! (
1645+ "expected IsrOverheadInflation for irq_src, got: {:#?}" ,
1646+ diags
1647+ )
1648+ } ) ;
1649+ assert ! (
1650+ inflation. message. contains( "WCET 100.00 us → 150.00 us" ) ,
1651+ "expected WCET 100us → 150us after 1×50us Interrupt_Overhead, got: {}" ,
1652+ inflation. message,
1653+ ) ;
1654+ assert ! (
1655+ inflation. message. contains( "1 × 50.00 us" ) ,
1656+ "diagnostic should make the 1× factor explicit, got: {}" ,
1657+ inflation. message,
1658+ ) ;
1659+ }
1660+
1661+ #[ test]
1662+ fn no_interrupt_overhead_byte_identical_to_pre_c1 ( ) {
1663+ // Interrupt_Overhead unset = no IsrOverheadInflation diagnostic
1664+ // and pre-v0.9.3 byte-identical RTA output (no inflation in the
1665+ // ISR's exec_wcet term).
1666+ let ( mut b, root, proc) = make_base ( ) ;
1667+ let dev = add_isr_device ( & mut b, root, "irq_src" , "2 ms" , "100 us" , 99 ) ;
1668+ let _ = dev;
1669+ let t1 = b. add_component ( "t1" , ComponentCategory :: Thread , Some ( proc) ) ;
1670+ b. set_children (
1671+ root,
1672+ vec ! [
1673+ ComponentInstanceIdx :: from_raw( la_arena:: RawIdx :: from_u32( 1 ) ) ,
1674+ proc,
1675+ dev,
1676+ ] ,
1677+ ) ;
1678+ b. set_children ( proc, vec ! [ t1] ) ;
1679+ bind_thread ( & mut b, t1, "10 ms" , "8 ms" , Some ( "10 ms" ) ) ;
1680+
1681+ let inst = b. build ( root) ;
1682+ let diags = RtaAnalysis . analyze ( & inst) ;
1683+
1684+ assert ! (
1685+ !diags
1686+ . iter( )
1687+ . any( |d| d. message. starts_with( "IsrOverheadInflation" ) ) ,
1688+ "no Interrupt_Overhead must not emit IsrOverheadInflation: {:#?}" ,
1689+ diags,
1690+ ) ;
1691+
1692+ // Same response time as the equivalent test in
1693+ // single_isr_reduces_task_capacity (8.40 ms or 8.50 ms,
1694+ // converged value).
1695+ let info = diags
1696+ . iter ( )
1697+ . find ( |d| d. severity == Severity :: Info && d. message . contains ( "thread 't1'" ) )
1698+ . expect ( "thread info present" ) ;
1699+ assert ! (
1700+ info. message. contains( "8.40 ms" ) || info. message. contains( "8.50 ms" ) ,
1701+ "absent Interrupt_Overhead must yield baseline RTA: {}" ,
1702+ info. message,
1703+ ) ;
1704+ }
1705+
1706+ #[ test]
1707+ fn interrupt_overhead_combined_with_context_switch_for_handler ( ) {
1708+ // A Sporadic ISR (device) with Interrupt_Overhead set inflates
1709+ // the ISR exec_wcet by exactly 1× per firing, while the
1710+ // separately-modeled handler thread (Bottom_Half_Server) has
1711+ // Context_Switch_Time set and gets the 2× task-side inflation.
1712+ // Both inflations must apply correctly and independently.
1713+ let ( mut b, root, proc) = make_base ( ) ;
1714+ let dev = b. add_component ( "irq_dev" , ComponentCategory :: Device , Some ( root) ) ;
1715+ b. set_property ( dev, "Timing_Properties" , "Period" , "2 ms" ) ;
1716+ b. set_property ( dev, "Spar_Timing" , "ISR_Execution_Time" , "100 us" ) ;
1717+ b. set_property ( dev, "Spar_Timing" , "ISR_Priority" , "99" ) ;
1718+ b. set_property ( dev, "Spar_Timing" , "Interrupt_Overhead" , "50 us" ) ;
1719+ b. set_property (
1720+ dev,
1721+ "Deployment_Properties" ,
1722+ "Actual_Processor_Binding" ,
1723+ "reference (cpu1)" ,
1724+ ) ;
1725+ b. set_property (
1726+ dev,
1727+ "Spar_Timing" ,
1728+ "Bottom_Half_Server" ,
1729+ "reference (handler)" ,
1730+ ) ;
1731+
1732+ let handler = b. add_component ( "handler" , ComponentCategory :: Thread , Some ( proc) ) ;
1733+ b. set_children (
1734+ root,
1735+ vec ! [
1736+ ComponentInstanceIdx :: from_raw( la_arena:: RawIdx :: from_u32( 1 ) ) ,
1737+ proc,
1738+ dev,
1739+ ] ,
1740+ ) ;
1741+ b. set_children ( proc, vec ! [ handler] ) ;
1742+ bind_thread ( & mut b, handler, "10 ms" , "1 ms" , Some ( "10 ms" ) ) ;
1743+ b. set_property (
1744+ handler,
1745+ "Thread_Properties" ,
1746+ "Dispatch_Protocol" ,
1747+ "Sporadic" ,
1748+ ) ;
1749+ b. set_property (
1750+ handler,
1751+ "Timing_Properties" ,
1752+ "Context_Switch_Time" ,
1753+ "100 us" ,
1754+ ) ;
1755+
1756+ let inst = b. build ( root) ;
1757+ let diags = RtaAnalysis . analyze ( & inst) ;
1758+
1759+ // 1× Interrupt_Overhead applied to ISR.
1760+ let isr_inflation = diags
1761+ . iter ( )
1762+ . find ( |d| {
1763+ d. severity == Severity :: Info
1764+ && d. message . starts_with ( "IsrOverheadInflation" )
1765+ && d. message . contains ( "'irq_dev'" )
1766+ } )
1767+ . unwrap_or_else ( || {
1768+ panic ! (
1769+ "expected IsrOverheadInflation for irq_dev, got: {:#?}" ,
1770+ diags
1771+ )
1772+ } ) ;
1773+ assert ! (
1774+ isr_inflation. message. contains( "WCET 100.00 us → 150.00 us" ) ,
1775+ "ISR side: 1×50us Interrupt_Overhead → 100us+50us = 150us, got: {}" ,
1776+ isr_inflation. message,
1777+ ) ;
1778+
1779+ // 2× Context_Switch_Time applied to handler thread.
1780+ let task_inflation = diags
1781+ . iter ( )
1782+ . find ( |d| {
1783+ d. severity == Severity :: Info
1784+ && d. message . starts_with ( "OverheadInflation" )
1785+ && d. message . contains ( "'handler'" )
1786+ } )
1787+ . unwrap_or_else ( || {
1788+ panic ! (
1789+ "expected OverheadInflation for handler thread, got: {:#?}" ,
1790+ diags
1791+ )
1792+ } ) ;
1793+ assert ! (
1794+ task_inflation. message. contains( "WCET 1.00 ms → 1.20 ms" ) ,
1795+ "task side: 2×100us Context_Switch_Time → 1ms+200us = 1.20ms, got: {}" ,
1796+ task_inflation. message,
1797+ ) ;
1798+ }
1799+
15801800 // ── B1: non-regression — no Locking_Protocol must match v0.7.0 byte-for-byte ─
15811801 #[ test]
15821802 fn no_locking_matches_v070 ( ) {
0 commit comments