RateLimitingAnnouncer

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
4 messages Options
Reply | Threaded
Open this post in threaded view
|

RateLimitingAnnouncer

Ben Coman

I am considering a use case where a complex UI rendering loop is linked
to an Announcement, which is being fired rapidly (eg < 1ms), and overall
system performance suffers. However the UI rendering loop really only
needs to execute every 100ms since that is sufficient for a user to
perceive an immediate response to their action.  So I have rolled-my-own
way to achieve this for which I was looking for some feedback...

1. Is there any existing feature in Pharo I have missed that can limit
the firing rate of an announcement ?
2. Would this be useful to others as a feature to ship with Pharo ? (so
I don't have to roll-my-own, and it gets more experienced eyes to ensure
its right :) )
3. General comments on my approach, improvements, alternatives.

By way of a use case using Workspace, execute...
-------------
| announcer mainCount  uiCount |
announcer := Announcer new. "RateLimitingAnnouncer new."
mainCount := uiCount := 0.
Transcript crShow: 'ui' ; tab; show: 'main' .
announcer subscribe: AnnouncementMockA do:
[     uiCount := uiCount + 1.
    "Imagine a longer duration UI rendering loop here"
    Transcript crShow: uiCount asString , '       ' , mainCount asString.
].
[    10000 timesRepeat:
    [    mainCount := mainCount + 1.
        "(Delay forMilliseconds: 1 ) wait."
         announcer announce: AnnouncementMockA new.    
    ]
] timeToRun .
-------------
With 'Announcer' this takes 10 seconds to execute with the last lines
being...
9999       9999
10000       10000

Replacing 'Announcer' with my own 'RateLimitingAnnouncer' listed below,
this takes 20 milliseconds with the last lines being...
ui    main
1       1
2       10000

Uncommenting the "Delay" takes 30 seconds for Announcer - and 15 seconds
for RateLimitingAnnouncer with last lines of...
289       9935
290       10000


Here is my code wrapping #announce: and #initialize...
-----
Announcer subclass: #RateLimitingAnnouncer
    instanceVariableNames: 'maxRateMilliSeconds queuedCounts'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'LEKtrek-Core'
-----
RateLimitingAnnouncer >>initialize
    super initialize .
    maxRateMilliSeconds := 100.
    queuedCounts := Dictionary new.
-----
RateLimitingAnnouncer >>announce: anAnnouncement
    | announcementClass |

    "Track how many announcements fired since reset at end of forked Delay"
    announcementClass := anAnnouncement class.
    queuedCounts at: announcementClass
        ifPresent: [ :count | queuedCounts at: announcementClass put:
count + 1 ]
        ifAbsent: [ queuedCounts at: announcementClass put: 1 ].

    "At first announcement since Delay'ed reset, forward this one and
set up Delay to condense subsequent ones. "    
    ( (queuedCounts at: announcementClass) = 1 ) ifTrue:
    [  
        [    "fire one announcement only for any announcement arriving
within delay period."
            (Delay forMilliseconds: maxRateMilliSeconds) wait.  
            [ (queuedCounts at: announcementClass) > 1 ] whileTrue:
            [    "At least one announcement arrived before end of Delay.
Forward one announcement only and repeat"  
                queuedCounts at: announcementClass put: 1.
                super announce: anAnnouncement.
                (Delay forMilliseconds: maxRateMilliSeconds) wait.  
            ].
            queuedCounts at: announcementClass put: 0.
        ] fork.  
        ^ super announce: anAnnouncement.      
    ].
-----

cheers -ben


Reply | Threaded
Open this post in threaded view
|

Re: RateLimitingAnnouncer

Stéphane Ducasse

On Jan 20, 2013, at 10:27 AM, Ben Coman wrote:

>
> I am considering a use case where a complex UI rendering loop is linked to an Announcement, which is being fired rapidly (eg < 1ms), and overall system performance suffers. However the UI rendering loop really only needs to execute every 100ms since that is sufficient for a user to perceive an immediate response to their action.  So I have rolled-my-own way to achieve this for which I was looking for some feedback...
>
> 1. Is there any existing feature in Pharo I have missed that can limit the firing rate of an announcement ?

not that I know :)

> 2. Would this be useful to others as a feature to ship with Pharo ? (so I don't have to roll-my-own, and it gets more experienced eyes to ensure its right :) )

> 3. General comments on my approach, improvements, alternatives.
>
> By way of a use case using Workspace, execute...
> -------------
> | announcer mainCount  uiCount |
> announcer := Announcer new. "RateLimitingAnnouncer new."
> mainCount := uiCount := 0.
> Transcript crShow: 'ui' ; tab; show: 'main' .
> announcer subscribe: AnnouncementMockA do:
> [     uiCount := uiCount + 1.
>   "Imagine a longer duration UI rendering loop here"
>   Transcript crShow: uiCount asString , '       ' , mainCount asString.
> ].
> [    10000 timesRepeat:
>   [    mainCount := mainCount + 1.
>       "(Delay forMilliseconds: 1 ) wait."
>        announcer announce: AnnouncementMockA new.       ]
> ] timeToRun .
> -------------
> With 'Announcer' this takes 10 seconds to execute with the last lines being...
> 9999       9999
> 10000       10000
>
> Replacing 'Announcer' with my own 'RateLimitingAnnouncer' listed below, this takes 20 milliseconds with the last lines being...
> ui    main
> 1       1
> 2       10000
>
> Uncommenting the "Delay" takes 30 seconds for Announcer - and 15 seconds for RateLimitingAnnouncer with last lines of...
> 289       9935
> 290       10000
>
>
> Here is my code wrapping #announce: and #initialize...
> -----
> Announcer subclass: #RateLimitingAnnouncer
>   instanceVariableNames: 'maxRateMilliSeconds queuedCounts'
>   classVariableNames: ''
>   poolDictionaries: ''
>   category: 'LEKtrek-Core'
> -----
> RateLimitingAnnouncer >>initialize
>   super initialize .
>   maxRateMilliSeconds := 100.
>   queuedCounts := Dictionary new.
> -----
> RateLimitingAnnouncer >>announce: anAnnouncement
>   | announcementClass |
>
>   "Track how many announcements fired since reset at end of forked Delay"
>   announcementClass := anAnnouncement class.
>   queuedCounts at: announcementClass
>       ifPresent: [ :count | queuedCounts at: announcementClass put: count + 1 ]
>       ifAbsent: [ queuedCounts at: announcementClass put: 1 ].
>
>   "At first announcement since Delay'ed reset, forward this one and set up Delay to condense subsequent ones. "       ( (queuedCounts at: announcementClass) = 1 ) ifTrue:
>   [          [    "fire one announcement only for any announcement arriving within delay period."
>           (Delay forMilliseconds: maxRateMilliSeconds) wait.              [ (queuedCounts at: announcementClass) > 1 ] whileTrue:            [    "At least one announcement arrived before end of Delay. Forward one announcement only and repeat"                  queuedCounts at: announcementClass put: 1.
>               super announce: anAnnouncement.
>               (Delay forMilliseconds: maxRateMilliSeconds) wait.              ].
>           queuedCounts at: announcementClass put: 0.
>       ] fork.          ^ super announce: anAnnouncement.          ].
> -----
>
> cheers -ben
>
>


Reply | Threaded
Open this post in threaded view
|

Re: RateLimitingAnnouncer

Fernando olivero-2
In reply to this post by Ben Coman
Hi, instead of subclassing Announcement, i would add the limiting logic to the one receiving the announcements. To keep everything but the view agnostic of this "optimization" detail. Because all you need is to prevent the UI from rendering in intervals less then 100ms. The announcement doesn't care wether the UI should redraw or not.


Fernando


On Sun, Jan 20, 2013 at 11:16 AM, Stéphane Ducasse <[hidden email]> wrote:

On Jan 20, 2013, at 10:27 AM, Ben Coman wrote:

>
> I am considering a use case where a complex UI rendering loop is linked to an Announcement, which is being fired rapidly (eg < 1ms), and overall system performance suffers. However the UI rendering loop really only needs to execute every 100ms since that is sufficient for a user to perceive an immediate response to their action.  So I have rolled-my-own way to achieve this for which I was looking for some feedback...
>
> 1. Is there any existing feature in Pharo I have missed that can limit the firing rate of an announcement ?

not that I know :)

> 2. Would this be useful to others as a feature to ship with Pharo ? (so I don't have to roll-my-own, and it gets more experienced eyes to ensure its right :) )

> 3. General comments on my approach, improvements, alternatives.
>
> By way of a use case using Workspace, execute...
> -------------
> | announcer mainCount  uiCount |
> announcer := Announcer new. "RateLimitingAnnouncer new."
> mainCount := uiCount := 0.
> Transcript crShow: 'ui' ; tab; show: 'main' .
> announcer subscribe: AnnouncementMockA do:
> [     uiCount := uiCount + 1.
>   "Imagine a longer duration UI rendering loop here"
>   Transcript crShow: uiCount asString , '       ' , mainCount asString.
> ].
> [    10000 timesRepeat:
>   [    mainCount := mainCount + 1.
>       "(Delay forMilliseconds: 1 ) wait."
>        announcer announce: AnnouncementMockA new.       ]
> ] timeToRun .
> -------------
> With 'Announcer' this takes 10 seconds to execute with the last lines being...
> 9999       9999
> 10000       10000
>
> Replacing 'Announcer' with my own 'RateLimitingAnnouncer' listed below, this takes 20 milliseconds with the last lines being...
> ui    main
> 1       1
> 2       10000
>
> Uncommenting the "Delay" takes 30 seconds for Announcer - and 15 seconds for RateLimitingAnnouncer with last lines of...
> 289       9935
> 290       10000
>
>
> Here is my code wrapping #announce: and #initialize...
> -----
> Announcer subclass: #RateLimitingAnnouncer
>   instanceVariableNames: 'maxRateMilliSeconds queuedCounts'
>   classVariableNames: ''
>   poolDictionaries: ''
>   category: 'LEKtrek-Core'
> -----
> RateLimitingAnnouncer >>initialize
>   super initialize .
>   maxRateMilliSeconds := 100.
>   queuedCounts := Dictionary new.
> -----
> RateLimitingAnnouncer >>announce: anAnnouncement
>   | announcementClass |
>
>   "Track how many announcements fired since reset at end of forked Delay"
>   announcementClass := anAnnouncement class.
>   queuedCounts at: announcementClass
>       ifPresent: [ :count | queuedCounts at: announcementClass put: count + 1 ]
>       ifAbsent: [ queuedCounts at: announcementClass put: 1 ].
>
>   "At first announcement since Delay'ed reset, forward this one and set up Delay to condense subsequent ones. "       ( (queuedCounts at: announcementClass) = 1 ) ifTrue:
>   [          [    "fire one announcement only for any announcement arriving within delay period."
>           (Delay forMilliseconds: maxRateMilliSeconds) wait.              [ (queuedCounts at: announcementClass) > 1 ] whileTrue:            [    "At least one announcement arrived before end of Delay. Forward one announcement only and repeat"                  queuedCounts at: announcementClass put: 1.
>               super announce: anAnnouncement.
>               (Delay forMilliseconds: maxRateMilliSeconds) wait.              ].
>           queuedCounts at: announcementClass put: 0.
>       ] fork.          ^ super announce: anAnnouncement.          ].
> -----
>
> cheers -ben
>
>



Reply | Threaded
Open this post in threaded view
|

Re: RateLimitingAnnouncer

Ben Coman
I see your point.  I guess I implemented it a bit broadly, and that some receivers of the same announcement may want rate limiting and while others do not. However I am working with the Glamour-Roassal framework, which predefines the use of announcements as... (#update: Announcement on: myAnnouncer) and it seemed easier to plug in a modified Announcer than to dig into the framework code.   By the time my layout code is reached, the framework has already discarded the previous view and handed me a blank one, so I had presumed that delaying the view creation and layouting would flicker the display - but I should test that properly.

Also I would prefer to keep the complexity of rate limiting announcement handling hidden from that part of my application code.  Perhaps I could define it at the receiver per subscription as ( Announcer>>subscribe: anAnnouncement noFasterThan: 100 do: [ something ] ) - but digging in there quickly gets above my current competency. 

Thanks for you thoughts.  I will consider it further.
cheers -Ben

Fernando Olivero wrote:
Hi, instead of subclassing Announcement, i would add the limiting logic to
the one receiving the announcements. To keep everything but the view
agnostic of this "optimization" detail. Because all you need is to prevent
the UI from rendering in intervals less then 100ms. The announcement
doesn't care wether the UI should redraw or not.


Fernando


On Sun, Jan 20, 2013 at 11:16 AM, Stéphane Ducasse <
[hidden email]> wrote:

  
On Jan 20, 2013, at 10:27 AM, Ben Coman wrote:

    
I am considering a use case where a complex UI rendering loop is linked
      
to an Announcement, which is being fired rapidly (eg < 1ms), and overall
system performance suffers. However the UI rendering loop really only needs
to execute every 100ms since that is sufficient for a user to perceive an
immediate response to their action.  So I have rolled-my-own way to achieve
this for which I was looking for some feedback...
    
1. Is there any existing feature in Pharo I have missed that can limit
      
the firing rate of an announcement ?

not that I know :)

    
2. Would this be useful to others as a feature to ship with Pharo ? (so
      
I don't have to roll-my-own, and it gets more experienced eyes to ensure
its right :) )

    
3. General comments on my approach, improvements, alternatives.

By way of a use case using Workspace, execute...
-------------
| announcer mainCount  uiCount |
announcer := Announcer new. "RateLimitingAnnouncer new."
mainCount := uiCount := 0.
Transcript crShow: 'ui' ; tab; show: 'main' .
announcer subscribe: AnnouncementMockA do:
[     uiCount := uiCount + 1.
  "Imagine a longer duration UI rendering loop here"
  Transcript crShow: uiCount asString , '       ' , mainCount asString.
].
[    10000 timesRepeat:
  [    mainCount := mainCount + 1.
      "(Delay forMilliseconds: 1 ) wait."
       announcer announce: AnnouncementMockA new.       ]
] timeToRun .
-------------
With 'Announcer' this takes 10 seconds to execute with the last lines
      
being...
    
9999       9999
10000       10000

Replacing 'Announcer' with my own 'RateLimitingAnnouncer' listed below,
      
this takes 20 milliseconds with the last lines being...
    
ui    main
1       1
2       10000

Uncommenting the "Delay" takes 30 seconds for Announcer - and 15 seconds
      
for RateLimitingAnnouncer with last lines of...
    
289       9935
290       10000


Here is my code wrapping #announce: and #initialize...
-----
Announcer subclass: #RateLimitingAnnouncer
  instanceVariableNames: 'maxRateMilliSeconds queuedCounts'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'LEKtrek-Core'
-----
RateLimitingAnnouncer >>initialize
  super initialize .
  maxRateMilliSeconds := 100.
  queuedCounts := Dictionary new.
-----
RateLimitingAnnouncer >>announce: anAnnouncement
  | announcementClass |

  "Track how many announcements fired since reset at end of forked Delay"
  announcementClass := anAnnouncement class.
  queuedCounts at: announcementClass
      ifPresent: [ :count | queuedCounts at: announcementClass put:
      
count + 1 ]
    
      ifAbsent: [ queuedCounts at: announcementClass put: 1 ].

  "At first announcement since Delay'ed reset, forward this one and set
      
up Delay to condense subsequent ones. "       ( (queuedCounts at:
announcementClass) = 1 ) ifTrue:
    
  [          [    "fire one announcement only for any announcement
      
arriving within delay period."
    
          (Delay forMilliseconds: maxRateMilliSeconds) wait.
      
 [ (queuedCounts at: announcementClass) > 1 ] whileTrue:            [
 "At least one announcement arrived before end of Delay. Forward one
announcement only and repeat"                  queuedCounts at:
announcementClass put: 1.
    
              super announce: anAnnouncement.
              (Delay forMilliseconds: maxRateMilliSeconds) wait.
      
     ].
    
          queuedCounts at: announcementClass put: 0.
      ] fork.          ^ super announce: anAnnouncement.          ].
-----

cheers -ben