[Laszlo-dev] For Review: Change 20071105-ptw-V Summary: Ensure deferred events do not recurse
P T Withington
ptw at pobox.com
Thu Dec 20 07:28:52 PST 2007
I am going to checkpoint the solution so far, which covers several of
your test cases, but not this super one. I need to get some other
stuff done, but don't want to lose this thread. Can you file a new
Jira item with this?
On 2007-12-04, at 15:06 EST, André Bargull wrote:
> I've got another "super" testcase for you, which shows again
> differences between queued and non-queued delegates...I guess you're
> starting to hate me...
>
> testcase:
> [code]
> <canvas debug="true" >
> <class name="test" extends="node" >
> <attribute name="foo" value="000" type="string"
> setter="setFoo(foo)" />
> <attribute name="bar" value="aaa" type="string"
> setter="setBar(bar)" />
> <event name="onfoo" />
> <event name="onbar" />
> <handler name="onfoo" args="foo" >
> Debug.write("onfoo with %s", foo);
> this.setBar(this.bar + foo);
> </handler>
> <handler name="onbar" args="bar" >
> Debug.write("onbar with %s", bar);
> this.setFoo(this.foo + bar);
> </handler>
> <method name="setFoo" args="foo" ><![CDATA[
> Debug.write("setFoo with %s", foo);
> this.foo = foo;
> this.onfoo.sendEvent(foo);
> ]]></method>
> <method name="setBar" args="bar" ><![CDATA[
> Debug.write("setBar with %s", bar);
> this.bar = bar;
> this.onbar.sendEvent(bar);
> ]]></method>
> </class>
> <test id="foobar" >
> <!-- later -->
> <handler name="oninit" reference="canvas" >
> Debug.write("-----");
> this.foo = "";
> this.bar = "";
> this.setBar("aaa");
> this.setFoo("000");
> </handler>
> </test>
> </canvas>
> [/code]
>
>
> i) current output (without your changeset applied):
> setBar with aaa
> setFoo with 000
> onbar with aaa
> setFoo with 000aaa
> onfoo with 000aaa
> setBar with aaa000aaa
> onbar with aaa000aaa
> setFoo with 000aaaaaa000aaa
> onfoo with 000
> setBar with aaa000aaa000
> onbar with aaa000aaa000
> setFoo with 000aaaaaa000aaaaaa000aaa000
> onfoo with 000aaaaaa000aaaaaa000aaa000
> setBar with aaa000aaa000000aaaaaa000aaaaaa000aaa000
> -----
> setBar with aaa
> onbar with aaa
> setFoo with aaa
> onfoo with aaa
> setBar with aaaaaa
> setFoo with 000
> onfoo with 000
> setBar with aaaaaa000
> onbar with aaaaaa000
> setFoo with 000aaaaaa000
>
>
> ii) output with your changeset:
> setBar with aaa
> setFoo with 000
> onbar with aaa
> setFoo with 000aaa
> onfoo with 000aaa
> setBar with aaa000aaa
> onfoo with 000
> setBar with aaa000aaa000
> -----
> setBar with aaa
> onbar with aaa
> setFoo with aaa
> onfoo with aaa
> setBar with aaaaaa
> setFoo with 000
> onfoo with 000
> setBar with aaaaaa000
> onbar with aaaaaa000
> setFoo with 000aaaaaa000
>
>
> iii) output of a prototype of mine (see attachment)
> setBar with aaa
> setFoo with 000
> onbar with aaa
> setFoo with 000aaa
> onfoo with 000aaa
> setBar with aaa000aaa
> onfoo with 000
> setBar with aaa000aaa000
> onbar with aaa000aaa000
> setFoo with 000aaaaaa000aaa000
> -----
> setBar with aaa
> onbar with aaa
> setFoo with aaa
> onfoo with aaa
> setBar with aaaaaa
> setFoo with 000
> onfoo with 000
> setBar with aaaaaa000
> onbar with aaaaaa000
> setFoo with 000aaaaaa000
>
>
> Summary:
> i) => too much function calls (+4)
> ii) => function calls are missing (-2)
> iii) => function calls match
>
> BUT, what do we actually expect for queued delegates?
>
>
> On 11/30/2007 6:41 PM, P T Withington wrote:
>> I updated the change to correctly handle your new test case. Would
>> you like to re-review it (http://svn.openlaszlo.org/openlaszlo/patches/20071105-ptw-V.tar
>> )
>>
>> I 'improved' your test case just a bit to make more sense to me:
>>
>> <canvas debug="true" >
>> <class name="foo" extends="node" >
>> <attribute name="bar" value="123" setter="setBar(bar)" />
>> <method name="setBar" args="bar" >
>> this.bar = bar;
>> if (this["onbar"]) {
>> this.onbar.sendEvent(bar);
>> }
>> </method>
>> <method name="provokeError" >
>> this.setBar("789");
>> </method>
>> <method name="provokeError2" >
>> this.setBar("xxx");
>> </method>
>> </class>
>> <foo bar="456" >
>> <handler name="onbar" args="bar">
>> Debug.write("onbar - 1 - begin with %s", bar);
>> this.provokeError();
>> Debug.write("onbar - 1 - end with %s", this.bar);
>> </handler>
>> <handler name="onbar" args="bar">
>> Debug.write("onbar - 2 - begin with %s", bar);
>> this.provokeError2();
>> Debug.write("onbar - 2 - end with %s", this.bar);
>> </handler>
>> <!-- later -->
>> <handler name="oninit" reference="canvas" >
>> Debug.write("-----");
>> this.setBar("abc");
>> </handler>
>> </foo>
>> </canvas>
>>
>> On 2007-11-06, at 19:28 EST, André Bargull wrote:
>>
>>> Approved.
>>>
>>> I've got another testcase which shows a different event-handling
>>> (comparing queued and non-queued delegates),
>>> even after applying this fix, but I don't know whether it is a bit
>>> too much constructed:
>>>
>>> <canvas debug="true" >
>>> <class name="foo" extends="node" >
>>> <attribute name="bar" value="123" setter="setBar(bar)" />
>>> <method name="setBar" args="bar" >
>>> this.bar = bar;
>>> if (this["onbar"]) {
>>> this.onbar.sendEvent(bar);
>>> }
>>> </method>
>>> <method name="provokeError" >
>>> this.setBar("789");
>>> </method>
>>> <method name="provokeError2" >
>>> this.setBar("xxx");
>>> </method>
>>> </class>
>>> <foo bar="456" >
>>> <handler name="onbar" args="bar">
>>> Debug.write("onbar - 1 - begin with %s", bar);
>>> this.provokeError();
>>> Debug.write("onbar - 1 - end with %s", bar);
>>> </handler>
>>> <handler name="onbar" args="bar">
>>> Debug.write("onbar - 2 - begin with %s", bar);
>>> this.provokeError2();
>>> Debug.write("onbar - 2 - end with %s", bar);
>>> </handler>
>>> <!-- later -->
>>> <handler name="oninit" reference="canvas" >
>>> Debug.write("-----");
>>> this.setBar("abc");
>>> </handler>
>>> </foo>
>>> </canvas>
>>>
>>> On 11/7/2007 12:23 AM, P T Withington wrote:
>>>> Change 20071105-ptw-V by ptw at dueling-banjos.local on 2007-11-05
>>>> 15:10:35 EST
>>>> in /Users/ptw/OpenLaszlo/ringding-2
>>>> for http://svn.openlaszlo.org/openlaszlo/trunk
>>>>
>>>> Summary: Ensure deferred events do not recurse
>>>>
>>>> Bugs Fixed:
>>>> LPP-4950 'LzDelegate single-execution mechanism does not work for
>>>> queued delegates'
>>>>
>>>> Technical Reviewer: hminksy (pending)
>>>> QA Reviewer: a.bargull at intensis.de (pending)
>>>>
>>>> Tests:
>>>> Test case from bug no longer shows the event recursing.
>>>>
>>>> Files:
>>>> M WEB-INF/lps/lfc/events/LaszloEvents.lzs
>>>>
>>>>
>>>> Changeset: http://svn.openlaszlo.org/openlaszlo/patches/20071105-ptw-V.tar
>>>>
>>
>>
>
> /**
> *
> * @copyright Copyright 2001-2007 Laszlo Systems, Inc. All Rights
> Reserved.
> * Use is subject to license terms.
> *
> * @affects lzevent lzdelegate
> * @topic LFC
> * @subtopic Events
> * @access public
> */
>
> /**
> * <p>
> * A delegate is an object that calls a specific method in a
> specific context
> * when its execute method is called. It is essentially a function
> pointer.
> * </p>
> * <p>
> * Delegates, along with <xref linkend="LzEvent"/>, comprise
> Laszlo's point to point event system. A delegate represents a named
> method of an instance. Delegates are mostly registered with events,
> but they can be used anywhere a function callback would
> traditionally be called for: for instance, the <xref
> linkend="LzTimer"/> service takes a delegate as its argument when a
> timer is started. See the code example on the <xref
> linkend="LzEvent"/>.
> * </p>
> *
> * @shortdesc The receiver in Laszlo's point-to-point event system.
> * @see LzEvent
> */
> class LzDelegate {
>
> /**
> *
> * @param Object context: reference to object which will be called
> * @param String functionName: name of the method to call (a string)
> * @param Object eventSender: optional; the sender of the event to
> register the
> * new delegate for.
> * @param String eventName: Optional, but required if eventSender is
> used; The name
> * of the event to register the new delegate for.
> */
> function initialize (context, functionName, eventSender, eventName) {
> super.initialize.apply(this, arguments);
> // too expensive to leave on all the time
> // if ($debug) {
> // this._dbg_created = Debug.backtrace();
> // }
> this.c = context;
> if ( $debug ){
> if (typeof(functionName) != "string")
> Debug.warn('LzDelegate functionName must be a string in %w',
> arguments.caller);
> }
> this.f = functionName;
> if ( eventSender != null ){
> if ( $debug ) {
> if (typeof(eventName) != "string") {
> Debug.warn('LzDelegate eventName must be a string in %w',
> arguments.caller);
> }
> }
> this.register( eventSender , eventName );
> }
>
> this.__delegateID = LzDelegate.__nextID++;
> }
>
> /** @access private */
> static var __nextID = 1;
>
> /** The context in which to call the method
> * @type Object
> */
> var c;
>
> /** The name of the method to call
> * @type String
> */
> var f;
>
> var lastevent = 0;
> var enabled = true;
> var event_called = false;
>
> /**
> * Executes the named method in the given context with the given
> data. Returns
> * the result of the call. In rare cases, this method may be
> overriden by a
> * subclass, and so it is marked 'protected' instead of 'private'.
> *
> * @param sd: The data with which to call the method.
> * @return: The value returned by the method call.
> * @access protected
> */
> function execute (sd){
> // Don't execute if context has been deleted, as that could
> // 'resurrect' the deleted view, causing a memory leak
> // Usually this is because a deleted view has idle or timer events
> // still registered.
> var context = this.c;
> if (context) {
> if (context['__LZdeleted']) {
> return;
> }
> return context[this.f]( sd );
> }
> }
>
> /**
> * Registers the delegate for the named event in the given context.
> <b>NB:</b>
> * This is the primary way in which events are created. Published
> events do
> * not generally exist as objects (with some exceptions) until
> delegates are
> * created for them.
> *
> * @param Object eventSender: The object which publishes the event.
> * @param String eventName: The name (string) of the event to
> register for.
> */
> function register ( eventSender , eventName){
> if (! eventSender) {
> if ($debug) {
> Debug.error('No eventSender (%w) for %s', eventSender,
> eventName);
> }
> return;
> }
>
> var anEvent = eventSender[ eventName ];
>
> if ( anEvent == LzDeclaredEvent || !anEvent ){
> // The call used to be unrolled here for swf. I removed it
> since it
> // was done during the swf5 days
> anEvent = new LzEvent( eventSender, eventName , this );
> } else {
> anEvent.addDelegate( this );
> }
>
> this[ this.lastevent++ ] = anEvent;
>
> // If debugging, add an informative name
> if ($debug) {
> var anEvent = eventSender[ eventName ];
> if (! anEvent.hasOwnProperty('_dbg_eventSender')) {
> anEvent._dbg_eventSender = eventSender;
> anEvent._dbg_eventName = eventName;
> // too expensive to leave on all the time
> // anEvent._dbg_created = Debug.backtrace();
> }
> }
> if ($profile) {
> var anEvent = eventSender[ eventName ];
> if (! anEvent.hasOwnProperty('_dbg_profileName')) {
> anEvent._dbg_profileName = eventName;
> }
> }
> }
>
> /**
> * Unregisters the delegate for all of the events it is registered
> for.
> */
> function unregisterAll ( ){
> for (var i = 0; i< this.lastevent ; i++){
> this[ i ].removeDelegate( this );
> this[ i ] = null;
> }
> this.lastevent = 0;
> }
>
> /**
> * Unregisters the delegate for the given event
> * @param LzEvent event: The event to unregister the delegate from.
> * (e.g. myview.onmouseup)
> */
> function unregisterFrom ( event ){
> var keep = [];
> for (var i = 0; i< this.lastevent ; i++){
> var ev = this[ i ];
> if ( ev == event ){
> ev.removeDelegate( this );
> } else {
> keep.push( ev );
> }
> this[ i ] = null;
> }
> //now fix it
> this.lastevent = 0;
> var l = keep.length;
> for ( var i = 0; i < l; i++ ){
> this[ this.lastevent++ ] = keep[ i ];
> }
> }
>
> /**
> * Disables the delegate until enable method is called.
> */
> function disable (){
> this.enabled = false;
> }
>
> /**
> * Enables a delegate that has been disabled
> */
> function enable (){
> this.enabled = true;
> }
>
> /**
> * Queue for delegates that are deferred during node initialization
> * @access private
> */
> static var __LZdelegatesQueue = [];
>
> static var __restoreEnv = {};
> static var __saveEnv = {};
>
> /**
> * Drain the delegates queue back to position POS
> * @access private
> */
> static function __LZdrainDelegatesQueue (pos) {
> var evq = this.__LZdelegatesQueue;
> var n = evq.length;
> var i = pos;
> if (i < n) {
> var calledDelegates = new Array;
> var lockedEvents = new Array;
> while (i < n) {
> var e = evq[i];
> if (e === LzDelegate.__saveEnv) {
> ++i;
> continue;
> } else if (e === LzDelegate.__restoreEnv) {
> var d;
> while (d = calledDelegates.pop()) {
> d.event_called = false;
> }
> var e;
> while (e = lockedEvents.pop()) {
> e.locked = false;
> }
> ++i;
> continue;
> }
> var d = evq[i+1];
> var sd = evq[i+2];
> // Mimic sendEvent which prohibits events and delegates from
> // recursing
> if (!e.locked) {
> e.locked = true;
> lockedEvents.push(e);
> }
> if (!d.event_called) {
> d.event_called = true; //this delegate has been called
> calledDelegates.push( d );
> // d.execute( sd ); inlined
> if (d.c[d.f]) d.c[d.f]( sd );
> }
> i+=3;
> }
> while (d = calledDelegates.pop()) {
> d.event_called = false;
> }
> while (e = lockedEvents.pop()) {
> e.locked = false;
> }
> }
> evq.length = pos;
> }
>
> /** @access private */
> function toString (){
> return ("Delegate for " + this.c + " calls " + this.f + " " +
> this.__delegateID );
> }
>
> if ($debug) {
> /**
> * If debugging, add an informative name
> * @access private
> */
> function _dbg_name (){
> var name = Debug.formatToString("%0.48w.%s()", this.c, this.f);
> if (this[0]) {
> name += Debug.formatToString("/* handles %w%s */", this[0],
> this[1]?" ...":"");
> }
> return name;
> }
> }
>
> } // End of LzDelegate
>
>
> /**
> * <p>Events underly most of the functionality in OpenLaszlo
> applications.
> * Unlike events in similar systems, OpenLaszlo's events are point-
> to-point, meaning that there is no general broadcast mechanism for
> events, and events do not trickle up or down the instance hierarchy.
> Instead, objects called <xref linkend="LzDelegate"/> <xref
> linkend="LzDelegate.prototype.register"/> for events, and if they
> try to register for an event that doesn't exist yet, the system
> creates the event. </p>
> *
> * <p>You can create a delegate explicitly using the <classname
> link="true">LzDelegate</classname> class, or implicitly by creating
> an handler.</p>
> * <p>
> * Because of the loose type requirements in LZX, calling an event
> that no delegate is listening for (and which therefore hasn't been
> created) has no effect. This allows objects to publish many more
> events than they actually need to create at runtime.
> * </p>
> *
> * <p>There are two syntaxes with which you can specify an event
> handler: in the tag used to create that object, or by using the
> * <tagname link="true">handler</tagname> tag.</p>
> *
> * <p>To specify an event handler in an object-creation tag, simply
> include it like any other attribute. For example,</p>
> *
> * <example class="code" extract="false">
> * <view onmouseover="doSomething()">
> * <method name="doSomething">
> * // code to be executed when mouse is over the view
> * </method>
> * </view>
> * </example>
> *
> * <p>If you use the <tagname link="true">handler</tagname> tag, you
> do not need to include the handler in the tag that creates the
> object.</p>
> *
> * <example class="code" extract="false">
> * <view>
> * <handler name="onmouseover">
> * // code to be executed when the mouse is over the view
> * </name>
> * </view>
> * </example>
> *
> * <p> The above two examples are functionally equivalent. Using
> the <tagname>handler</tagname> tag, however, can often lead to more
> readable code because it removes clutter from the object creation
> tag.</p>
> *
> *
> * <p>Use the <tagname link="true">event</tagname> tag to create the
> events; then use the <method>sendEvent</method> method to dispatch
> it. The following example illustrates how to create custom events.</p>
> *
> * <example class="program"
> * title="A simple example of publishing and listening for
> a custom event">
> * <canvas height="40">
> * <simplelayout/>
> * <button name="eventSender"
> * <em>onmouseover="this.customevent.sendEvent()"
> * onmouseout="this.customevent.sendEvent()"/></em>
> *
> * <event name="customevent"/>
> * <view bgcolor="red" width="20" height="20"
> oninit="this.setupDelegate()">
> * <method name="setupDelegate">
> * this.del = new LzDelegate( this, "respondToEvent" );
> * <em>this.del.register( eventSender , "customevent" );</em>
> * </method>
> * <method name="respondToEvent">
> *
> * this.setAttribute('x', this.x + 10);
> * </method>
> * </view>
> * </canvas>
> * </example>
> *
> * <p>
> * Events can be sent with a single argument, which usually conveys
> information about the property that changed. The default behavior of
> the <xref linkend="LzNode.prototype.setAttribute"/> method is to set
> the named property and send the event called "on" + property. This
> is general mechanism that updates constraints in a OpenLaszlo
> programs. For instance, when a view changes its <attribute>x</
> attribute> position, it sends the event <event>onx</event> with the
> new value for its <attribute>x</attribute> property.
> *
> * </p>
> *
> * <example class="program"
> * title="Event sending in response to setting an attribute">
> * <canvas height="40">
> * <simplelayout/>
> * <button name="eventSender"
> * <em>onmouseover="this.setAttribute('avalue',
> this.avalue + 10)"
> * onmouseout="this.setAttribute('avalue', this.avalue +
> 5)"</em>>
> * <attribute name="avalue" value="0"/>
> * </button>
> *
> * <view bgcolor="red" width="20" height="20"
> oninit="this.setupDelegate()">
> * <method name="setupDelegate">
> * this.del = new LzDelegate(this, "respondToEvent");
> * this.del.register(eventSender, <em>"onavalue"</em>);
> * </method>
> * <method name="respondToEvent" args="v">
> * this.setAttribute('x' , v);
> * </method>
> *
> * </view>
> * </canvas>
> * </example>
> *
> * @shortdesc The sender in Laszlo's point-to-point event system.
> * @see LzDelegate
> */
>
> class LzEvent {
>
> /**
> * @param Object eventSender: The owner of this event
> * @param String eventName: The name of this event.
> */
> function initialize ( eventSender , eventName , d ){
> super.initialize.apply(this, arguments);
>
> var _evs = eventSender._events;
> if (_evs == null ){
> eventSender._events = [ this ];
> } else {
> _evs.push ( this );
> }
> eventSender[ eventName ] = this;
> if ( d ){
> this.delegateList = [d];
> this.ready = true;
> }else{
> this.delegateList = [];
> }
>
> // If debugging, add an informative name
> if ($debug) {
> this._dbg_eventSender = eventSender;
> this._dbg_eventName = eventName;
> }
> if ($profile) {
> this._dbg_profileName = eventName;
> }
> }
>
> // Because this is not created with Class
> //prototype.classname = "LzEvent";
>
> /** True when event is being sent.
> * @type Boolean
> */
> var locked = false;
>
> /** True when event is ready to be sent.
> *
> */
> var ready = false;
>
> /**
> * Adds the given delegate to the event's delegate list. Although
> this listed
> * as a public function it should rarely be called explicitly -- it
> is used
> * exclusively by <b><i>LzDelegate</i>.register</b>
> *
> * @param LzDelegate d: The delegate to add to the list of
> delegates called by the event.
> */
> function addDelegate (d){
> this.ready = true;
> this.delegateList.push(d);
> }
>
>
>
> /**
> * Sends the event, passing its argument as the data to all the
> called
> * delegates
> *
> * @param sd: The data to send with the event.
> */
> function sendEvent ( sd ){
> if ( this.locked ) { return; } //don't allow for multiple calls
>
> var dll = this.delegateList.length;
> if (dll == 0) { return; }
>
> if ($profile) {
> var nm = this._dbg_profileName;
>
> if (nm) {
> Profiler.event(nm, 'calls');
> }
> }
>
> this.locked = true;
>
> var calledDelegates = new Array;
>
> var d;
> var evq = LzDelegate.__LZdelegatesQueue;
> var evqFlag = false;
> for (var i = dll; i >= 0; i--){
> d = this.delegateList[ i ];
> //pointer may be bad due to deletions
> if ( d && ! d.event_called){
> d.event_called = true; //this delegate has been called
> calledDelegates.push( d );
> // We don't worry about deleted contexts here, because
> // we assume that delegates registered on events are
> // properly managed
> if (d.enabled && d.c) {
> if (d.c.__LZdeferDelegates) {
> if (!evqFlag) {
> evqFlag = true;
> evq.push(LzDelegate.__saveEnv);
> }
> evq.push(this, d, sd);
> } else {
> // d.execute( sd ); inlined
> if (d.c[d.f]) d.c[d.f]( sd );
> }
> }
> }
> }
>
> if (evqFlag) {
> evq.push(LzDelegate.__restoreEnv);
> }
>
> while (d = calledDelegates.pop() ){
> d.event_called = false;
> }
>
> if ($profile) {
> var nm = this._dbg_profileName;
> if (nm) {
> Profiler.event(nm, 'returns');
> }
> }
>
> this.locked = false;
> }
>
> /**
> * Removes the delegate from the delegate list. In practice, this
> is rarely
> * called explicitly, since it does not update the delegate's list
> of stored
> * events. Right now, this is called only by <b><i>LzDelegate</i>.
> * unregisterAll</b> Delegates should support a simple unregister
> command, that
> * unregisters them for a single event, but to date, that has not
> proven
> * necessary
> *
> * @param LzDelegate d: The delegate to remove from the delegateList.
> */
> function removeDelegate ( d ){
> var dll = this.delegateList.length;
> for (var i = 0; i < dll; i++){
> if (this.delegateList[i] == d){
> this.delegateList.splice(i, 1);
> break;
> }
> }
> if ( this.delegateList.length == 0 ){
> this.ready = false;
> }
> }
>
> /**
> * Removes all delegates from call list
> */
> function clearDelegates (){
> while (this.delegateList.length ){
> this.delegateList[ 0 ].unregisterFrom( this );
> }
> //this.delegateList = [];
> this.ready = false;
> }
>
> /**
> * Returns the number of delegates registered for the event
> * @return Number: The number of delegates registered for the event.
> */
> function getDelegateCount ( ){
> return this.delegateList.length;
> }
>
> function toString (){
> return ( "LzEvent");
> }
>
> if ($debug) {
> /**
> * If debugging, add an informative name
> * @access private
> */
> function _dbg_name (){
> return Debug.formatToString("%0.48w.%s", this._dbg_eventSender,
> this._dbg_eventName);
> }
> }
>
> } // End of LzEvent
More information about the Laszlo-dev
mailing list