View | Details | Raw Unified | Return to bug 315999
Collapse All | Expand All

(-)class/corlib/System.Threading/Timer.cs (-123 / +283 lines)
Lines 4-9 Link Here
4
// Authors:
4
// Authors:
5
// 	Dick Porter (dick@ximian.com)
5
// 	Dick Porter (dick@ximian.com)
6
// 	Gonzalo Paniagua Javier (gonzalo@ximian.com)
6
// 	Gonzalo Paniagua Javier (gonzalo@ximian.com)
7
// 	Rafael Ferreira (raf@ophion.org)
7
//
8
//
8
// (C) 2001, 2002 Ximian, Inc.  http://www.ximian.com
9
// (C) 2001, 2002 Ximian, Inc.  http://www.ximian.com
9
// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
10
// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
Lines 29-34 Link Here
29
//
30
//
30
31
31
using System.Runtime.InteropServices;
32
using System.Runtime.InteropServices;
33
using System.Collections;
34
using System;
32
35
33
namespace System.Threading
36
namespace System.Threading
34
{
37
{
Lines 37-161 Link Here
37
#endif
40
#endif
38
	public sealed class Timer : MarshalByRefObject, IDisposable
41
	public sealed class Timer : MarshalByRefObject, IDisposable
39
	{
42
	{
40
		sealed class Runner : MarshalByRefObject
43
		/*
41
		{
44
		
42
			ManualResetEvent wait;
45
		Timer Scheduler
43
			AutoResetEvent start_event;
46
		---------------
44
			TimerCallback callback;
47
		Author: Rafael Ferreira (raf@ophion.org)
45
			object state;
48
		
46
			int dueTime;
49
		The code below implements a single thread scheduler that fires
47
			int period;
50
		events using the runtime's built-in thread pool.
48
			bool disposed;
49
			bool aborted;
50
51
51
			public Runner (TimerCallback callback, object state, AutoResetEvent start_event)
52
		Key Features:
52
			{
53
			Single thread scheduler:
53
				this.callback = callback;
54
				A single thread handles firing all timer jobs thus allowing a 
54
				this.state = state;
55
				much greater number of Timers to be defined
55
				this.start_event = start_event;
56
			Lazy init:
56
				this.wait = new ManualResetEvent (false);
57
				Timer scheduler is only started after the first System.Threading.Timer is created
57
			}
58
			Early termination:
59
				Timer scheduler thread dies if there are no more timer jobs in its Job queue
60
				
61
			
62
		In a nutshell the scheduler works like this:
63
			1 - The main scheduler thread (TimerScheduler) wakes up and finds out what time it is
64
			2 - The scheduler iterates over the list of timer jobs (Jobs) to find out when the next job is
65
			as well as fires all timers that were scheduled to run now or in the past
66
			3 - The Scheduler then calculates the mutiplier for its sleep algorithm. The multiplier
67
			is basically MSEC_UNTIL_NEXT_JOB / TIME_SLICE where TIME_SLICE is the minimum amount of 
68
			time the scheduler thread is allowed to sleep for
69
			4 - Sleep for multiplier * TIME_SLICE
70
			5 - Goto 1
58
71
59
			public int DueTime {
72
		Possible improvements:
60
				get { return dueTime; }
73
			* Convert the big for-loop into a sorted data structure. This will speed up the time
61
				set { dueTime = value; }
74
			it takes the scheduler to iterate over the timer jobs and lower CPU usage under insane 
62
			}
75
			amounts of Timers 
63
76
64
			public int Period {
77
		Possible issues:
65
				get { return period; }
78
			* Overflow issues with the multiplier
66
				set { period = value == 0 ? Timeout.Infinite : value; }
79
			* Race conditions with lazy-init of the scheduler thread.
67
			}
68
80
69
			bool WaitForDueTime ()
81
		Note:
70
			{
82
			* MONO_TIMER_DEBUG environment variable can be used to turn on the scheduler's debug log
71
				if (dueTime > 0) {
83
			* MONO_TIMER_USETP environment variable can be used to force the timer scheduler to use 
72
					bool signaled;
84
			the CLI's built in thread pool
73
					do {
85
			* sync_obj is used to make signaling the scheduler class sane so only one signal is sent at a
74
						wait.Reset ();
86
			time and a decision can be made on the signal so signals are "batched" or discarded 
75
						signaled = wait.WaitOne (dueTime, false);
76
					} while (signaled == true && !disposed && !aborted);
77
87
78
					if (!signaled)
88
		*/
79
						callback (state);
80
89
81
					if (disposed)
90
		// timer metadata:
82
						return false;
91
		internal long NextRun;
83
				}
92
		internal long LastRun;
84
				else
93
		internal long Period;
85
					callback (state);
94
		internal bool Enabled;
95
		internal TimerCallback Callback;
96
		internal int ID;
97
		internal object State = null;
98
		
99
		// flag that turns on "verbose logging"
100
		static bool debug_enabled = false;
101
		
102
		// flag that tells scheduler to use the cli's thread pool
103
		static bool use_threadpool = false;
104
		
105
		// mininum sleep time
106
		const int TIME_SLICE = 10 ; // 10 msec
107
		
108
		// next Timer ID;
109
		static int next_id = 0;
86
110
87
				return true;
111
		// timer scheduler - there can only be one
88
			}
112
		static Thread scheduler = null;
113
		
114
		// used to enforce thread safety
115
		static object sync_obj = new object();
116
		
117
		static Hashtable Jobs =  new Hashtable();
89
118
90
			public void Abort ()
119
		static bool scheduler_ready = false;
91
			{
120
		
92
				lock (this) {
121
		// this enum is used to signal the scheduler thread of the reason for the Abort() call
93
					aborted = true;
122
		// abort() is used to signal the timer thread since Interrupt() is not implemented
94
					wait.Set ();
123
		enum AbortSignals { TimerAdded, TimerRemoved, TimerChanged };
95
				}
124
		
125
		sealed class Runner: MarshalByRefObject
126
		{
127
			Timer timer;
128
			public Runner(Timer t) {
129
				this.timer = t;
130
			
96
			}
131
			}
132
			public void Run() {
97
			
133
			
98
			public void Dispose ()
134
				timer.Callback(timer.State);
99
			{
135
				
100
				lock (this) {
136
				
101
					disposed = true;
102
					Abort ();
103
				}
104
			}
137
			}
138
		}
139
		void SetProperties (int dueTime, int period)
140
		{
141
			if (dueTime == Timeout.Infinite) {
142
				//disables the job
143
				log("disabling timer " + ID);
144
				Enabled = false;
145
			}else {
146
				NextRun = DateTime.Now.Ticks + TimeSpan.TicksPerMillisecond * dueTime;
147
				Enabled = true;
148
			}
149
			if (period == Timeout.Infinite) {
150
				log("timer " + ID  + " will only run once");
151
				Period = -1;
152
			}else {
153
				Period = TimeSpan.TicksPerMillisecond * period;
154
			}
155
			log(String.Format("timer configured, id {2} delay {0} msec period {1} msec", dueTime, period, ID));
105
156
106
			public void Start ()
157
		}
107
			{
158
		// prorperly handles signaling the scheduler thread
108
				while (!disposed && start_event.WaitOne ()) {
159
		// it will retry for 500 msec (.5 sec) if it can't properly signal scheduler
109
					if (disposed)
160
		// NOT THREAD SAFE
110
						return;
161
		void SendSchedulerSignal (AbortSignals signal) 
162
		{
163
			for (int i = 0; i < 100; i++) {
111
164
112
					aborted = false;
165
				// we don't start a new scheduler if the signal is TimerRemoved
166
				if (scheduler == null) {
167
					log("Scheduler not currently running... new scheduler will be initiated");
168
					scheduler = new Thread( new ThreadStart(SchedulerThread));
169
					scheduler.IsBackground = true;
170
					scheduler.Start();
171
					return;
172
					
173
				} 
113
174
114
					if (dueTime == Timeout.Infinite)
175
				if (scheduler.ThreadState == ThreadState.AbortRequested || 	scheduler.ThreadState == ThreadState.Aborted) {
115
						continue;
176
					return;
177
				}
116
178
117
					if (!WaitForDueTime ())
179
				if (scheduler_ready) {
118
						return;
180
					// we batch send Abort() calls
181
					// Abort is used since Thread.Interrupt is not supported.
182
					scheduler.Abort(signal);
183
					return;
184
					
185
				}
186
				log("could not properly signal timer-scheduler, waiting...");
187
				Thread.Sleep(5); 
188
			}
189
			
190
			throw new Exception("Could not properly abort timer-scheduler thread");
191
			
192
			
193
		}
119
194
120
					if (aborted || (period == Timeout.Infinite))
195
		public void SchedulerThread () 
121
						continue;
196
		{
197
			Thread.CurrentThread.Name = "Timer-Scheduler";
198
			long tick = 0;
199
			long next_job = Int64.MaxValue;
200
			Timer tj = null;
201
			int multiplier = 1;
202
			
203
			// big scary for-loop that iterates over the jobs
204
			while(Jobs.Count > 0) {
205
				if (!scheduler_ready)  {
206
					scheduler_ready=true;
207
					log("Scheduler is ready");
208
				}
122
209
123
					bool signaled = false;
210
				try {
124
					while (true) {
211
					tick = DateTime.Now.Ticks;
125
						if (disposed)
212
					lock (Jobs) {
126
							return;
213
						foreach (DictionaryEntry entry in Jobs) {
214
							tj = entry.Value as Timer;
127
215
128
						if (aborted)
216
							if (tj.Enabled == false) {
129
							break;
217
								continue;
218
							}
219
							if ( tj.NextRun <= tick) {
220
								
221
								// Firing job 
222
								log("Firing job " + tj.ID);
223
								dispatch(tj);
224
								
225
								if (tj.Period == - 1) {
226
									// it is a run-once job, so we disable it
227
									tj.Enabled = false;
228
								}
229
								else {
230
									tj.NextRun = tick + tj.Period;
231
								}
232
								
233
								tj.LastRun = tick;
130
234
131
						try {
235
								// we reset the next_job to the max possible value so the real next job
132
							wait.Reset ();
236
								// can be figured out
133
						} catch (ObjectDisposedException) {
237
								next_job = Int64.MaxValue;
134
							// FIXME: There is some race condition
238
							}
135
							//        here when the thread is being
239
							if ( next_job > tj.NextRun) {
136
							//        aborted on exit.
240
								next_job = tj.NextRun;
137
							return;
241
							}
138
						}
242
						}
139
243
					}
140
						signaled = wait.WaitOne (period, false);
244
					
141
245
					// no other jobs are available and all timers
142
						if (aborted || disposed)
246
					// are disabled
143
							break;
247
					if (next_job == Int64.MaxValue) {
144
248
						log("no active timers found, going into infinite sleep");
145
						if (!signaled) {
249
						Thread.Sleep(Timeout.Infinite);
146
							callback (state);
250
						
147
						} else if (!WaitForDueTime ()) {
251
					}else {
148
							return;
252
						multiplier = (int) ((next_job - tick) / TimeSpan.TicksPerMillisecond);
253
						multiplier = multiplier / TIME_SLICE;
254
						if (multiplier > 0 ) {
255
							//TODO there are some edgy race conditions between the abort signal and telling a thread 
256
							// to sleep
257
							log("gong to sleep for " + multiplier + " times the time slice");
258
							Thread.Sleep(multiplier * TIME_SLICE);
149
						}
259
						}
150
					}
260
					}
261
					
262
				} catch (ThreadAbortException ex) {
263
					if (ex.ExceptionState is AbortSignals) {
264
						log(String.Format("abort signal received: {0}",ex.ExceptionState));
265
						switch((AbortSignals)ex.ExceptionState) {
266
							default:
267
								Thread.ResetAbort();
268
								break;
269
						}
270
					}else {
271
						log(ex.Message);
272
					}
273
				}catch (Exception ex) {
274
					log("generic exception caught by the scheduler");
275
					log(ex.Message);
151
				}
276
				}
277
					
152
			}
278
			}
279
			scheduler_ready = false;
280
			scheduler = null;
281
			log("timer scheduler is shutting down");
153
		}
282
		}
154
283
155
		Runner runner;
284
		void log (string str)
156
		AutoResetEvent start_event;
285
		{
157
		Thread t;
286
			if (debug_enabled)
287
				Console.Error.WriteLine(String.Format("{0} TIMER SCHEDULER: {1}",DateTime.Now,str));
288
		}
158
289
290
		void dispatch(Timer timer) {
291
			
292
			// should we use the thread pool? 
293
			if(use_threadpool) {
294
				ThreadPool.QueueUserWorkItem(new WaitCallback(timer.Callback),timer.State);
295
				return;
296
			}
297
			
298
			// let's just fire up a new thread to handle running the timer
299
			Runner runner = new Runner(timer);
300
			Thread t = new Thread(new ThreadStart(runner.Run));
301
			t.IsBackground = true;
302
			t.Start();			
303
			
304
		}
159
		public Timer (TimerCallback callback, object state, int dueTime, int period)
305
		public Timer (TimerCallback callback, object state, int dueTime, int period)
160
		{
306
		{
161
			if (dueTime < -1)
307
			if (dueTime < -1)
Lines 198-209 Link Here
198
344
199
		void Init (TimerCallback callback, object state, int dueTime, int period)
345
		void Init (TimerCallback callback, object state, int dueTime, int period)
200
		{
346
		{
201
			start_event = new AutoResetEvent (false);
347
			if (!debug_enabled) {
202
			runner = new Runner (callback, state, start_event);
348
				if(Environment.GetEnvironmentVariable("MONO_TIMER_DEBUG") != null)
203
			Change (dueTime, period);
349
					debug_enabled = true;
204
			t = new Thread (new ThreadStart (runner.Start));
350
			}
205
			t.IsBackground = true;
351
			
206
			t.Start ();
352
			if (!use_threadpool) {
353
				if(Environment.GetEnvironmentVariable("MONO_TIMER_USETP") != null) {
354
					log("timer will dispatch using the thread pool");	
355
					use_threadpool = true;
356
				}
357
			}			
358
			ID = Interlocked.Increment(ref next_id);
359
		
360
			// first run take into consideration the delay metric only
361
			SetProperties(dueTime,period);
362
			Callback = callback;
363
			State = state;
364
		
365
			// lock job Q
366
			lock(Jobs) {
367
				Jobs.Add(ID,this);
368
369
			}
370
			lock(sync_obj) {
371
				SendSchedulerSignal(AbortSignals.TimerAdded);
372
			}
207
		}
373
		}
208
374
209
		public bool Change (int dueTime, int period)
375
		public bool Change (int dueTime, int period)
Lines 214-230 Link Here
214
			if (period < -1)
380
			if (period < -1)
215
				throw new ArgumentOutOfRangeException ("period");
381
				throw new ArgumentOutOfRangeException ("period");
216
382
217
			if (runner == null)
383
			// modifying the job is actually quicker (lock wise) than doing a Remove / Add combo
218
				return false;
384
			lock (Jobs) {
219
385
				if (!Jobs.Contains(ID)) {
220
			start_event.Reset ();
386
					return(false);
221
			runner.Abort ();
387
				}
222
			runner.DueTime = dueTime;
388
				SetProperties(dueTime,period);
223
			runner.Period = period;
389
				log("job " + ID +" changed");
224
			start_event.Set ();
390
			}
391
			lock (sync_obj) {
392
				SendSchedulerSignal(AbortSignals.TimerChanged);
393
			}
225
			return true;
394
			return true;
226
		}
395
		}
227
228
		public bool Change (long dueTime, long period)
396
		public bool Change (long dueTime, long period)
229
		{
397
		{
230
			if(dueTime > 4294967294)
398
			if(dueTime > 4294967294)
Lines 255-269 Link Here
255
423
256
		public void Dispose ()
424
		public void Dispose ()
257
		{
425
		{
258
			if (t != null && t.IsAlive) {
426
			lock (Jobs) {
259
				if (t != Thread.CurrentThread)
427
				if (Jobs.Contains(ID)) { 
260
					t.Abort ();
428
					Jobs.Remove(ID);
261
				t = null;
429
					lock (sync_obj) {
430
						SendSchedulerSignal(AbortSignals.TimerRemoved);
431
					}
432
					log(String.Format("Job {0} removed",ID));
433
				}
262
			}
434
			}
263
			if (runner != null) {
264
				runner.Dispose ();
265
				runner = null;
266
			}
267
			GC.SuppressFinalize (this);
435
			GC.SuppressFinalize (this);
268
		}
436
		}
269
437
Lines 274-287 Link Here
274
			return true;
442
			return true;
275
		}
443
		}
276
444
277
		~Timer ()
278
		{
279
			if (t != null && t.IsAlive)
280
				t.Abort ();
281
282
			if (runner != null)
283
				runner.Abort ();
284
		}
285
	}
445
	}
286
}
446
}
287
447
(-)class/corlib/System.Threading/ChangeLog (+3 lines)
Lines 1-3 Link Here
1
2006-08-10  Rafael Ferreira <raf@ophion.org>
2
	* Timer.cs: changed timer logic to be single threaded as opposed to n:n
3
1
2006-07-04  Atsushi Enomoto  <atsushi@ximian.com>
4
2006-07-04  Atsushi Enomoto  <atsushi@ximian.com>
2
5
3
	* WaitHandle.cs : CheckArray() is also used in WaitAny(), so added
6
	* WaitHandle.cs : CheckArray() is also used in WaitAny(), so added
(-)class/corlib/Test/System.Threading/TimerTest.cs (-51 / +38 lines)
Lines 3-8 Link Here
3
//
3
//
4
// Author:
4
// Author:
5
//   Zoltan Varga (vargaz@freemail.hu)
5
//   Zoltan Varga (vargaz@freemail.hu)
6
//   Rafael Ferreira (raf@ophion.org)
6
//
7
//
7
// (C) 2004 Novell, Inc (http://www.novell.com)
8
// (C) 2004 Novell, Inc (http://www.novell.com)
8
//
9
//
Lines 23-132 Link Here
23
	// -- Ben
24
	// -- Ben
24
	//
25
	//
25
	public class TimerTest : Assertion {
26
	public class TimerTest : Assertion {
27
		// this bucket is used to avoid non-theadlocal issues
28
		class Bucket {
29
			public int count;
30
		}
31
		
26
		[Test]
32
		[Test]
27
		[Category ("NotWorking")]
28
		public void TestDueTime ()
33
		public void TestDueTime ()
29
		{
34
		{
30
			counter = 0;
35
			Bucket bucket = new Bucket();
31
			Timer t = new Timer (new TimerCallback (Callback), null, 200, Timeout.Infinite);
36
			Timer t = new Timer (new TimerCallback (Callback), bucket, 200, Timeout.Infinite);
32
			Thread.Sleep (50);
37
			Thread.Sleep (50);
33
			AssertEquals ("t0", 0, counter);
38
			AssertEquals ("t0", 0, bucket.count);
34
			Thread.Sleep (200);
39
			Thread.Sleep (200);
35
			AssertEquals ("t1", 1, counter);
40
			AssertEquals ("t1", 1, bucket.count);
36
			Thread.Sleep (500);
41
			Thread.Sleep (500);
37
			AssertEquals ("t2", 1, counter);
42
			AssertEquals ("t2", 1, bucket.count);
38
			
39
			t.Change (10, 10);
43
			t.Change (10, 10);
40
			Thread.Sleep (500);
44
			Thread.Sleep (1000);
41
			Assert ("t3", counter > 20);
45
			Assert ("t3", bucket.count > 20);
42
			t.Dispose ();
46
			t.Dispose ();
43
		}
47
		}
44
48
45
		[Test]
49
		[Test]
46
		[Category ("NotWorking")]
47
		public void TestChange ()
50
		public void TestChange ()
48
		{
51
		{
49
			counter = 0;
52
			Bucket bucket = new Bucket();
50
			Timer t = new Timer (new TimerCallback (Callback), null, 1, 1);
53
			Timer t = new Timer (new TimerCallback (Callback), bucket, 1, 1);
51
			Thread.Sleep (500);
54
			Thread.Sleep (500);
52
			int c = counter;
55
			int c = bucket.count;
53
			Assert ("t1", c > 20);
56
			Assert ("t1", c > 20);
54
			t.Change (100, 100);
57
			t.Change (100, 100);
55
			Thread.Sleep (500);
58
			Thread.Sleep (500);
56
			Assert ("t2", counter <= c + 6);
59
			Assert ("t2", bucket.count <= c + 6);
57
			t.Dispose ();
60
			t.Dispose ();
58
		}
61
		}
59
62
60
		[Test]
63
		[Test]
61
		[Category ("NotWorking")]
62
		public void TestZeroDueTime () {
64
		public void TestZeroDueTime () {
63
			counter = 0;
65
			Bucket bucket = new Bucket();
64
66
65
			Timer t = new Timer (new TimerCallback (Callback), null, 0, Timeout.Infinite);
67
			Timer t = new Timer (new TimerCallback (Callback), bucket, 0, Timeout.Infinite);
66
			Thread.Sleep (100);
68
			Thread.Sleep (100);
67
			AssertEquals (1, counter);
69
			AssertEquals (1, bucket.count);
68
			t.Change (0, Timeout.Infinite);
70
			t.Change (0, Timeout.Infinite);
69
			Thread.Sleep (100);
71
			Thread.Sleep (100);
70
			AssertEquals (2, counter);
72
			AssertEquals (2, bucket.count);
71
			t.Dispose ();
73
			t.Dispose ();
72
		}
74
		}
73
74
		[Test]
75
		[Test]
75
		[Category ("NotWorking")]
76
		public void TestDispose ()
76
		public void TestDispose ()
77
		{
77
		{	
78
			counter = 0;
78
			Bucket bucket = new Bucket();
79
			Timer t = new Timer (new TimerCallback (CallbackTestDispose), null, 10, 10);
79
			Timer t = new Timer (new TimerCallback (Callback), bucket, 10, 10);
80
			Thread.Sleep (200);
80
			Thread.Sleep (200);
81
			t.Dispose ();
81
			t.Dispose ();
82
			Thread.Sleep (20);
82
			Thread.Sleep (20);
83
			int c = counter;
83
			int c = bucket.count;
84
			Assert (counter > 5);
84
			Assert (bucket.count > 5);
85
			Thread.Sleep (200);
85
			Thread.Sleep (200);
86
			AssertEquals (c, counter);
86
			AssertEquals (c, bucket.count);
87
		}
87
		}
88
88
89
		[Test] // bug #78208
89
		[Test] // bug #78208
90
		public void TestDispose2 ()
90
		public void TestDispose2 ()
91
		{
91
		{
92
			Timer t = new Timer (new TimerCallback (CallbackTestDispose), null, 10, 10);
92
			Timer t = new Timer (new TimerCallback (Callback), null, 10, 10);
93
			t.Dispose ();
93
			t.Dispose ();
94
			t.Dispose ();
94
			t.Dispose ();
95
		}
95
		}
96
		
96
	
97
		[Test]
97
		[Test]
98
		[Category ("NotWorking")]
98
		[Category("NotWorking")]
99
		public void TestDisposeOnCallback () {
99
		public void TestDisposeOnCallback () {
100
			counter = 0;
100
		
101
			t1 = new Timer (new TimerCallback (CallbackTestDisposeOnCallback), null, 0, 10);
101
			Timer t1 = null;
102
			t1 = new Timer (new TimerCallback (CallbackTestDisposeOnCallback), t1, 0, 10);
102
			Thread.Sleep (200);
103
			Thread.Sleep (200);
103
			AssertNull (t1);
104
			AssertNull(t1);
104
			
105
			
105
			counter = 2;
106
			t1 = new Timer (new TimerCallback (CallbackTestDisposeOnCallback), null, 50, 0);
107
			Thread.Sleep (200);
108
			AssertNull (t1);
109
		}
106
		}
110
		
107
111
		private void CallbackTestDisposeOnCallback (object foo)
108
		private void CallbackTestDisposeOnCallback (object foo)
112
		{
109
		{
113
			if (++counter == 3) {
110
			((Timer)foo).Dispose();
114
				t1.Dispose ();
115
				t1 = null;
116
			}
117
		}
111
		}
118
112
119
		private void CallbackTestDispose (object foo)
120
		{
121
			counter++;
122
		}
123
124
		private void Callback (object foo)
113
		private void Callback (object foo)
125
		{
114
		{
126
			counter++;
115
			Bucket b = foo as Bucket;
116
			b.count++;
127
		}
117
		}
128
129
		Timer t1;
130
		int counter;
131
	}
118
	}
132
}
119
}
(-)class/corlib/Test/System.Threading/ChangeLog (+3 lines)
Lines 1-3 Link Here
1
2006-08-10 Rafael Ferreira <raf@ophion.org>
2
	* TimerTest.cs: Added a bucket object to avoid thread-local issues on failing tests
3
1
2006-06-14  Sebastien Pouliot  <sebastien@ximian.com>
4
2006-06-14  Sebastien Pouliot  <sebastien@ximian.com>
2
5
3
	* ExecutionContextTest.cs: Changed Run test to execute only under
6
	* ExecutionContextTest.cs: Changed Run test to execute only under

Return to bug 315999