Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Topic/thread reschedule #5038

Merged
merged 14 commits into from
Jul 19, 2022
Merged

Conversation

jamshark70
Copy link
Contributor

Purpose and Motivation

New feature suggested in #4903: Make it possible to reschedule a Routine, Task or EventStreamPlayer transparently onto a different clock or to a different (later) time.

Along the way, I found out that our documentation for nextTimeOnGrid is quite poor. I haven't updated it yet. It's tangentially related to this PR, so I'm not sure if I should just fold it in here or make another PR. (nextTimeOnGrid doesn't change substantially here, except for a new referenceBeat argument. Default behavior is unchanged.)

(There is one unrelated documentation cleanup: Task help said that Condition could not be used within a Task. That hasn't been true for some years, so I took the liberty of removing that remark.)

Types of changes

  • Documentation
  • New feature

(The nextTimeOnGrid third argument could be a breaking change if someone has created an extension with additional arguments for this method selector. AFAICS within the main class library, there is no breaking change.)

To-do list

  • Code is tested
  • All tests are passing
  • Updated documentation
  • This PR is ready for review

@jamshark70 jamshark70 added enhancement comp: class library SC class library comp: help schelp documentation labels Jun 26, 2020
@telephon
Copy link
Member

A really good new feature, thank you. I'll test it.

};
clock = argClock;
};
if(this === thisThread) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there is a good name for what happens here, I think this should be a method on function, similar to forkIfNeeded

Copy link
Contributor Author

@jamshark70 jamshark70 Jun 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would it look? If it's a method of Function, then you wouldn't have implicit access to the thread as this, so maybe it would have to be like this, in context of this PR: { ... }.deferFromThread(this/*.threadPlayer*/). (I'm not sure this is the place to bikeshed a new method though.)

(Edit) I'm tempted to suggest a method of Thread/PauseStream; then, using function-style syntax: deferFromThread(aThread) { ... } (though it reads slightly strangely using message syntax: aThread.deferFromThread { ... } but that isn't too bad).

Copy link
Member

@telephon telephon Jun 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the place to bikeshed a new method though

As a non native speaker, the meaning of bikeshedding remains obscure to me (I'm pretty sure I've never done it, although I do have a bicycle ;-)).

Joke aside, this definitely is the place to discuss how to factor a problem. This may include a new method, and the general usefulness of such a method. If it turns out to be a large discussion, we can still do this in a separate thread.

I'm tempted to suggest a method of Thread/PauseStream

I think your second version is good, if we assume that the intention is to defer away from or defer off a given thread. This method makes clear what reading the code leaves obscure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've done that (and even instinctively named the method deferAwayFrom).

I have to admit, writing the documentation for the new method renewed my doubts about it (though its usages in both reschedule implementations do look cleaner). But, let's leave that to the review process.

@jamshark70
Copy link
Contributor Author

Joke aside, this definitely is the place to discuss how to factor a problem.

It's a delicate balance. We've already seen some PRs spiral out of control. This PR isn't in danger of that... I think I probably err on the side of keeping PRs close to the initial scope.

I don't mind to change it here. It'll be a couple of days later.

@telephon
Copy link
Member

It's a delicate balance. We've already seen some PRs spiral out of control.

This forum is not necessarily the adequate place for developer cooperation, as you said, it depends.

@jamshark70
Copy link
Contributor Author

I'd like to add the WIP tag for now -- I just realized that this is implemented for streams based on Routines, but not for any variety of FuncStream. That would be an exceptional usage but for completeness, it should be there.

@jamshark70
Copy link
Contributor Author

I just realized that this is implemented for streams based on Routines, but not for any variety of FuncStream. That would be an exceptional usage but for completeness, it should be there.

I'm afraid I will have to give up on this. I couldn't figure out how to get the intrinsic variables to work.

Currently, we have Routine : Thread : Stream : AbstractFunction : Object (cf. Routine.superclasses = same). Object, AbstractFunction and Stream have no instance variables (slots). Thread has a lot of them, and they are defined as intrinsic variables in PyrObject.cpp. (Routine doesn't declare any additional ones.)

This PR adds rescheduledTime to Thread (inherited by Routine) -- so, reschedule is defined for Thread, and separately for PauseStream.

FuncStream inherits from Stream, not Thread. That suggests moving var rescheduledTime to Stream -- which means moving one of the intrinsic variables. I couldn't figure out how to do this without crashing sclang on startup. (For that matter, it isn't clear how intrinsic variables work with inheritance.)

I'm also reconsidering that maybe this isn't a big deal. You can play a Pfunc, and rescheduling works because it's really an EventStreamPlayer that's accessing the FuncStream.

(
p = Pfunc {
	var delta = exprand(0.05, 0.25);
	(midinote: rrand(48, 72), dur: delta, legato: 0.1)
}.play;
)

p.reschedule(quant: 3);  // works

p.stop;

But you can't do the same with FuncStream:

(
p = FuncStream({
	var delta = exprand(0.05, 0.25);
	(midinote: rrand(48, 72), dur: delta, legato: 0.1).play;
	delta  // return/reschedule
}).play;
)

p.reschedule(quant: 3);  // Message 'reschedule' not understood

thisProcess.stop;  // it's not a stream player so, no 'stop'

... but in almost 20 years, I've never seen anyone do this -- not least because there's no convenient way to stop it.

I had been thinking of the case of p = aPattern.collect { ... }.play but that's a Pcollect, analogous to Pfunc above which is already working -- and p = aPattern.asStream.collect { ... }.play doesn't work at all (and never did) -- so, you always need the pattern wrapper anyway.

Except here:

r = Routine {
	var delta;
	loop {
		delta = exprand(0.05, 0.25);
		(midinote: rrand(48, 72), dur: delta, legato: 0.1).play;
		delta.wait
	}
};

q = r.collect { |x| x * 2 }.play;

q.stop;  // doesn't stop!
thisProcess.stop;  // so have to do this :-\

... but nobody does that either, because of the same problem (no way to stop q).

TL;DR except for abstracting out deferFromThread (in a moment...), I think we can leave this as this, and move forward to review.

@telephon
Copy link
Member

I agree that it would be too much to expect the full scheduling abilities on FuncStream. In general, it is always a good idea to improve uniformity in the stream system, but this feature is in line with other, similar particularities, as you say. So let's go ahead with this as it is.

Copy link
Member

@telephon telephon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor suggestions.

discussion::

list::
## Rescheduling to a different clock will be continuous in terms of seconds, but not necessarily continuous in terms of beats. Time-based patterns such as link::Classes/Pseg::, or the use of link::Classes/Env:: as a stream, are likely to break.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"are likely to break." is both too strong and too imprecise. Maybe better: "may behave not as expected"?

list::
## Rescheduling to a different clock will be continuous in terms of seconds, but not necessarily continuous in terms of beats. Time-based patterns such as link::Classes/Pseg::, or the use of link::Classes/Env:: as a stream, are likely to break.

When switching the clock, code::reschedule:: will first calculate the new "awake" time, in beats, based on the given code::quant::. Then it converts that code::beats:: value into the new clock's code::beats:: for the same instant in time. If the old clock's tempo is 1 and the Routine was waiting for 1 beat, the next "awake" should happen after one second. When rescheduling without a code::quant::, the next "awake" will emphasis::still:: happen after one second -- but the code::beats:: value will adjust for the new clock.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the old clock's tempo is 1 and the Routine was waiting for 1 beat, the next "awake" should happen after one second.

Not sure I understand. If it "was waiting", then the time has passed -- it should happen instantly?

Copy link
Contributor Author

@jamshark70 jamshark70 Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First question, then, is: Do you think I should explain it in complete, exhaustive detail, or just give a high-level overview and let users infer the behavior from examples?

The complete explanation goes something like this:

There are four relevant moments in time:

A. When the thread last awoke.
B. Now.
C. When the thread is next scheduled to awake.
D. The rescheduled time. This may be the same moment as C, but translated to a different clock; or later than C (on the same or a different clock).

While it's supported to reschedule a thread from within the thread, often the reschedule caller will be outside the thread. In that case, B is not constrained to line up with any of the routine times: A <= B < C <= D (if B == C, behavior might be undefined).

If it "was waiting", then the time has passed -- it should happen instantly?

"... was waiting for 1 beat" means: at A, a 1.0.wait call occurred. So we know C = A + 1, and B must be within 1 beat of A. From the reference point of B, the time may not have passed.

Rescheduling does not change C -- even if you reschedule onto a different clock, the thread will awake at time C. I think that's why I used past tense here -- you can't change a previously issued wait (whereas, if I said it "is waiting," you might think you could make it "be waiting" for a different length of time). (And, if D == translateToOtherClock(C), then the thread will continue from the same moment when it would have awakened, but with a new beats base and possibly a different tempo.)

I think it should be clear by now why I'm struggling with the right balance between clarity and concision. The above is too much detail for one method's documentation -- but trying to streamline the discussion ends up confusing some crucial details.


Switch the Task to a different clock, or a different time, without stopping. See link::Classes/Routine#-reschedule:: for complete documentation.

NOTE:: If you want to reschedule a Task from within the Task itself, code::thisThread.reschedule(...):: will not work (as it does in a Routine). You must write code::thisThread.threadPlayer.reschedule(...):: instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better:
If you want to reschedule a Task from within the Task itself, code::thisThread.reschedule(...):: will not work, because the task is what plays the thread. This is unlike in a Routine, which plays itself.

SCClassLibrary/Common/Core/Clock.sc Show resolved Hide resolved
@jamshark70
Copy link
Contributor Author

I believe I addressed the review comments. (And, on second thought, I changed that "was waiting" to "is waiting" -- maybe it's OK now? Though that section is difficult to explain.)

@telephon
Copy link
Member

Thank you, this reads very well, and the nextTimeOnGrid solution for IdentityDictionary is also convincing.

@jamshark70
Copy link
Contributor Author

the nextTimeOnGrid solution for IdentityDictionary is also convincing.

I'd like to take another look at this interface tomorrow, to be sure it will really work for rescheduling.

@jamshark70
Copy link
Contributor Author

I think my suspicion that last night was correct -- my change to IdentityDictionary:nextTimeOnGrid was not right.

TempoClock:nextTimeOnGrid calls quant.asQuant -- so, whatever object is returned by IdentityDictionary:asQuant needs to follow the same method signature as Quant:nextTimeOnGrid, namely aQuant.nextTimeOnGrid(clock, referenceBeat).

IdentityDictionary says asQuant { ^this.copy }. Therefore it should be anIdentityDictionary.nextTimeOnGrid(clock, referenceBeat) -- the reference beat must be passed in at the time of resolving the quant. (Also, the reference beat is not a property of a Quant object -- so it shouldn't be a property of a dictionary being used as a quant. Editorial Confusion introduced by adding a new feature without a proper functional spec -- I know we'll never really start writing functional specs before doing code changes, but it really does concretely cause problems 🤣 )

Committed a fix.

@jamshark70 jamshark70 force-pushed the topic/ThreadReschedule branch from fb6d696 to e94e908 Compare July 19, 2020 01:42
@telephon
Copy link
Member

In my experience, the best thing is always a page of concise examples.

@joshpar
Copy link
Member

joshpar commented Feb 20, 2022

@telephon @jamshark70 - sorry about the delay on this PR - what is the current consensus? Should we pull this in to 3.13, and if so, anything else needed?

@joshpar joshpar added this to the 3.13.0 milestone Feb 20, 2022
@jamshark70
Copy link
Contributor Author

what is the current consensus? Should we pull this in to 3.13, and if so, anything else needed?

At this point, I don't remember. It generally looks OK...?

@telephon
Copy link
Member

Yes, I think it is good.

@dyfer
Copy link
Member

dyfer commented Jul 14, 2022

I resolved a merge conflict in the helpfile (I think).

I assume this is okay to merge, but because the conversation was stalled a while back I'll wait a bit before pulling this @telephon @jamshark70

@jamshark70
Copy link
Contributor Author

IIRC this is ready...? Thanks for fixing the conflict.

@dyfer dyfer merged commit 2ecd997 into supercollider:develop Jul 19, 2022
@woutersnoei
Copy link
Contributor

Hi folks, there seems to be an issue with this merge, considering PauseStream/Task when playing on AppClock. Reproduce like this:
Task({ "hello".postln }, AppClock ).play;

--> ERROR: Message 'schedAbs' not understood.

This is because AppClock doesn't understand schedAbs..

@dyfer
Copy link
Member

dyfer commented Aug 23, 2022

@jamshark70 @telephon what do you think about the issue brought up by @woutersnoei ?

@telephon
Copy link
Member

telephon commented Aug 23, 2022

hm, I never really understood why AppClock doesn't understand schedAbs (apart from the fact that it won't be exactly an absolute time, but that is true for everything on AppClock). I've run into this before …

@dyfer
Copy link
Member

dyfer commented Aug 23, 2022

there seems to be an issue with this merge, considering

@woutersnoei after seeing response from @telephon I checked and this issue was introduced in the current development version of SC, after 3.12, but before this PR. It is reported in #5700. It would be good to fix it before releasing 3.13 as it is a regression (AFAIU). Let's continue the conversation in #5700.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comp: class library SC class library comp: help schelp documentation enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants