はじめに
TouchDesigner Advent Calendar 2019 11日目の記事です。
TouchDesignerで各種システムを実装する場合、「遅延なく1msでも早くこの関数を実行したい」とか「必ずこの順序で機能が実行されることを担保したい」など、関数実行の順序で夜も眠れないことが幾夜もありました。
そこで、今回はCHOPExecuteやpythonスクリプトからのCHOPパラメータ変更、コンポーネント内関数実行の実行順序に性質について今年のうちに確認してみることにしました。
手っ取り早く最後にまとめがあります。
toeはこちら
https://github.com/shks/TDCHOPExecuteOrderTest
実行環境
- macOS 10.14.2
- TouchDesigner 2019.17550
検証方法
pythonコード内実行順序で確認
import time
print('python ' , 1 , 1000*time.time())
print('python ' , 2 , 1000*time.time())
print('python ' , 3 , 1000*time.time())
python >>>
python 1 1576067145201.466
python 2 1576067145201.4822
python 3 1576067145201.489
ここで、text1 DAT
を選択した状態でcontrol + R
によってpythonスクリプトを実行することができます。今回時間計測はtimeモジュールのtime.time()
を用います。windowsとmacで挙動が異なる、time.perf_counter()
のほうが好ましい等の情報もありますが、今回は遅延時間計測よりも順序の検証が主な目的なので、time.time()
をそのまま使っています。数字はms単位にして表示します。
python実行での出力は、textportにて確認できます。ここで、毎計測ごとにclearをしないとprint関数実行自体の遅延が発生するので、計測毎にclearして観測します。
CHOP並列接続 CHOP Execute実行
まず、ネットワーク上で「値が**になったら〇〇する」といった場合に用いるCHOP Executeの実行順序を調査します。
下記のようなネットワークで、ボタンを押すことで、null CHOP
のnull1
, null2
, null3
を参照している、CHOP Execute
が実行されます。これらの実行順序はどのように決定しているのか。
null CHOP
の生成順序は1,2,3、chopexec1 Dat
の生成順序も1,2,3となる。ネットワーク接続順番も、1,2,3の順番で生成している。下記の図では、丸で囲まれた数字がtextportで確認した実行順序を示す。
import time
def onOffToOn(channel, sampleIndex, val, prev):
print("chop exec - onOffToOn " , me.digits ,1000 * time.time())
return
結果
python >>>
chop exec - onOffToOn 3 1576067851541.211
chop exec - onOffToOn 2 1576067851541.3289
chop exec - onOffToOn 1 1576067851541.434
これを見る限り、なにか1, 2, 3の逆順で実行されているようです。
オペレーターの名前で変わるのであろうか?
null2
→ null99
, chopexec2
→ chopexec99
のように、名前を変更する。
python >>>
chop exec - onOffToOn 3 1576067912511.506
chop exec - onOffToOn 99 1576067912511.7139
chop exec - onOffToOn 1 1576067912511.945
しかし、実行順序は変わらない。つまり、名前の順序順序ではない。(なお、配置を変えても変わらない)となると、では、
- CHOPオペレーターの生成順序なのか
- DATオペレーターの生成順序に依存するのか?
- もしくはCHOPオペレーター同士を接続するネットの順番か?
並列に接続されたCHOPにそれぞれに、逆順で生成したchopexec DATを参照させる。
python >>>
chop exec - onOffToOn 3 1575998873208.3982
chop exec - onOffToOn 2 1575998873208.636
chop exec - onOffToOn 1 1575998873208.7642
null Chop
の生成順序は1,2,3、chopexec1 Dat
の生成順序も3,2,1。よって上記の「DATオブジェクトの生成順序」ではない。
オペレーター同士を接続するネットワークの生成順序か?
上記のネットワークのnull_x
と null*
の接続を切った後に、下記の順番で再接続。実行順番は変わるか?図中のネットワークに記載した番号はネットワークの生成順序を示す。
ネットワークの生成順序で実行順序が変わる。例として、下記の場合は
python >>>
chop exec - onOffToOn 1 1576068005900.81
chop exec - onOffToOn 3 1576068005900.926
chop exec - onOffToOn 2 1576068005901.0278
結果、上記3つですべて実行順序が異なり、ネットワーク生成順序の(若い方)新しく生成されたネットワークから先に伝搬し、chopexecDat
が実行されることを確認。
では、一つのCHOP
に複数のchopexec
が参照されていたらどうなるのか。
並列に接続されたCHOPにそれぞれに、複数のchopexec DATを参照させる。
ここでもおなじく、つなげた順番で若い順で実行される。上記で、chopexec2_created_2
,chopexec2_created_4
とchopexec2_created_5
は、この順番でオペレーターを生成したが、
下記の順番にnull2_2への参照(リンク)を行った。
chopexec2_created_2
chopexec2_created_5
chopexec2_created_4
その場合、CHOP
に接続されたchopexec DAT
の実行順序は
python >>>
chop exec - onOffToOn 7 1576002202557.483
chop exec - onOffToOn 1 1576002202557.6028
chop exec - onOffToOn 4 1576002202557.711
chop exec - onOffToOn 5 1576002202557.908
chop exec - onOffToOn 2 1576002202558.051
chop exec - onOffToOn 6 1576002202558.2139
chop exec - onOffToOn 3 1576002202558.403
ここで、chopexec2_created_4
が、chopexec2_created_5
よりも早く実行されているが分かる。
よって、chopexec Dat
への参照リンクが若い方から実行される。
(なお、生成した後に名前を変えたり、digit
(オペレーターの名前を変更しても挙動は変わらない)
CHOP直列接続 chop executeDat実行
python >>>
chop exec - onOffToOn 3 1575997858135.001
chop exec - onOffToOn 2 1575997858135.2651
chop exec - onOffToOn 1 1575997858135.4722
このように、後ろから実行されているようだが、これは、接続の終端からだろうか?それとも生成順であろうか。そこで次のネットワークでは、同じだか生成タイミングと接続順序が異なる状況でCHOP Execute
を実行させる。
python >>>
chop exec - onOffToOn 4 1575998953775.958
chop exec - onOffToOn 6 1575998953776.133
chop exec - onOffToOn 5 1575998953776.284
chop exec - onOffToOn 3 1575998953776.4722
chop exec - onOffToOn 2 1575998953776.708
chop exec - onOffToOn 1 1575998953776.9019
直列に接続されている場合には、終端のオペレーターに参照されているchopexec
から実行される。その歳には、オペレーターの生成順序は関係ない。なお、ここでnull4
,null5
,null6
に`接続しているネットワークを後から付け足したので、4,6,5が先に実行されている。
つまり、分岐が存在した場合には、若いネットワークの方を探索し、その終端から順番に実行される。
上記のネットワークを下記のように変えると、*マークのネットワークのほうが新しい(若い)接続になるので、下記のように終端の新しい接続から先に実行される。
python >>>
chop exec - onOffToOn 4 1576000433919.5671
chop exec - onOffToOn 3 1576000433920.164
chop exec - onOffToOn 6 1576000433920.728
chop exec - onOffToOn 5 1576000433921.278
chop exec - onOffToOn 2 1576000433921.777
chop exec - onOffToOn 1 1576000433922.294
もう十分ですね。はい。
CHOP Execute直列/並列実行
このように、chopexec1内でop('constant4').par.value0 = 1, op('constant3').par.value0 = 1 のように、constantCHOP オペレーターの値を直接書き込んでしまう方法もある。この場合の実行順序はpythonスクリプトでのアクセス順序であろうか。
op('constant2').par.value0 = 1
op('constant4').par.value0 = 1
op('constant3').par.value0 = 1
この場合、その場合には、pythonに記載した順序で変更先で伝搬した値に基づいてchopexec3
, chopexec4
が実行される。
python >>>
chop exec 1 1576006026799.0889
chop exec 2 1576006026799.2761
python >>>
chop exec 4 1576006026799.5068
chop exec 3 1576006026799.634
pythonスクリプト内のchopの読み書き
TouchDesignerではpython上で動作ロジックを構築することも多くありますが、そのpythonからCHOP
等の様々なオペレーターの値を取得したり、更新することができます。それらの処理が行われるタイミングに関して調べてみます。
まずは、pythonスクリプト実行途中で、ConstantCHOP
等に読み書きを行った場合、”即時”に値が反映されているのであろうか?
text_read_and_write
内のpythonスクリプトで3つのconstant
に対して値を書き込んで、かつ値を読んでprint出力します。かつ、その値の差分を計算して、書き込んだ値が入っているかを確認します。これを見る限り、pythonスクリプトでConstantCHOP
のパラメータへ値を代入した時点で代入値が保持され、次の行ではすでにその値を取得できることがわかります。
import time
import random
def functionExe(index):
r =random.randint(1,10)
#write
t = time.time()
op('constant' + str(index)).par.value0 = r
print('writetime ' , str(index) , (time.time() - t) * 1000 , 'ms', r )
#read
t = time.time()
v = op('constant' + str(index))[0]
print('readtime ' , str(index) , (time.time() - t) * 1000 , 'ms', v )
print('write-read' , str(index) , v - r )
for i in range(1,6):
functionExe(i)
python >>>
writetime 1 0.03910064697265625 ms 5
readtime 1 0.032901763916015625 ms 5.0
write-read 1 0.0
writetime 2 0.019073486328125 ms 1
readtime 2 0.015974044799804688 ms 1.0
write-read 2 0.0
writetime 3 0.014066696166992188 ms 10
readtime 3 0.0050067901611328125 ms 10.0
write-read 3 0.0
では、pythonスクリプトから更新したconstantChop
から接続されているNullCHOP
に入る値はどのようになるのでしょうか?
Pythonスクリプト内からのCHOP読み書きとネットワーク接続の関係
pythonスクリプトから、あるConstantCHOP
の値を更新し、その先に接続しているNullChop
の値をpythonスクリプト上から取得する。
すると、CHOP
へ書込みの値とCHOP
からの読み出しの値が同じ値で、printには0.0と出ていることが確認できます。
write - read 1 0.0
write - read 2 0.0
write - read 3 0.0
結果:
pythonからあるCHOP
を更新し、その先に接続しているChopにpythonからアクセスすると、更新処理が伝搬する。(これは、平たく言うと、pythonからあるChopを更新すると、その接続先のChopまで更新処理が伝搬する。厳密には違う)
厳密には上図の「3」が呼ばれたときに「2」のCookが実行され、更新された値を「3」で取得できるという流れであるが、そうであっても実行順序は1, 2, 3のようになる。
詳しくはこちら→https://docs.derivative.ca/Cook
The Order of Cooking
TouchDesigner is a "pull system". A common misconception with cooking in TouchDesigner is that cooking starts upstream and moves downstream.
Almost all operators will only cook when something is interested in their data.
謎:
それでは、このようにpythonスクリプトから、Constant CHOP
の値を変更した場合、前述の CHOPExcute
の実行順序はどのようになるのでしょうか?値が変更されているのならば、cook(処理)が走って値が変更された時点で、CHOPExcute
スクリプトが実行されるのでしょうか?
Pythonスクリプト内からのCHOP読み書きとネットワーク途中のCHOPExcuteの関係
先程の終端にあったnullChop
にonValueChange
で関数を実行するようにします。
import time
def onValueChange(channel, sampleIndex, val, prev):
print("chop exec - read" , me.digits ,1000 * time.time())
return
そこで上記と同じように、constantCHOP
とnullCHOP
を接続して、
constant1
の値書き換え、null1
から値読み出し、
constant2
の値書き換え、null2
から値読み出し、
constant3
の値書き換え、null3
から値読み出し
を行います。上記の実験から、null1
読み出しを行った時点で、constant1
-null1
の更新部分は実行されているので、そのタイミングで、onValueChange()
も実行されているのでは、と期待してしまいそうになりますが。。
python >>>
write - read 1 0.0 1576057360065.77
write - read 2 0.0 1576057360065.836
write - read 3 0.0 1576057360065.887
chop exec - read 1 1576057360066.437
chop exec - read 2 1576057360066.59
chop exec - read 3 1576057360066.747
結果は、最初にpythonスクリプト内の処理(nullChop1
, nullChop2
, nullChop3
からの値読み出しを含む)が完了した後に、chopexec_1
,chopexec_2
, chopexec_3
のonValueChange()
が順次実行されています。
結果:
pythonスクリプト内の処理が完了した後に、CHOPExecuteが順次実行される。
謎:
では、python内部で1回以上、constantChop
の値を書き換えた場合には、どのようになるのでしょうか。onValueChange
は二度実行されるのか。
Pythonスクリプト内からの複数のCHOP読み書きとネットワーク途中のChop excute関係
単純に、上記のスクリプトを2度、textDat
のpython実行で繰りした。但し、printで表示される値は、書込み値と、読み込み値を別で表示している。
import time
import random
###FIRST
r =random.randint(1,10)
#1
op('constant1').par.value0 = r
v = op('null1')[0]
print(1, 'write', r, 'read', v,1000 * time.time())
r =random.randint(1,10)
#2
op('constant2').par.value0 = r
v = op('null2')[0]
print(2, 'write', r, 'read', v,1000 * time.time())
r =random.randint(1,10)
#3
op('constant3').par.value0 = r
v = op('null3')[0]
print(3, 'write', r, 'read', v,1000 * time.time())
###SECOND
r =random.randint(1,10)
#1
op('constant1').par.value0 = r
v = op('null1')[0]
print(1, 'write', r, 'read', v,1000 * time.time())
r =random.randint(1,10)
#2
op('constant2').par.value0 = r
v = op('null2')[0]
print(2, 'write', r, 'read', v,1000 * time.time())
r =random.randint(1,10)
#3
op('constant3').par.value0 = r
v = op('null3')[0]
print(3, 'write', r, 'read', v,1000 * time.time())
1 write 8 read 8.0 1576058581321.446 ms
2 write 4 read 4.0 1576058581321.513 ms
3 write 2 read 2.0 1576058581321.5588 ms
1 write 8 read 8.0 1576058581321.5881 ms
2 write 1 read 1.0 1576058581321.626 ms
3 write 10 read 10.0 1576058581321.6628 ms
chop exec - read 1 1576058581322.1929 ms
chop exec - read 2 1576058581322.376 ms
chop exec - read 3 1576058581322.56 ms
pythonスクリプト上では、constantChop
の値書込みと、その接続されてるnullChop
からの値読み込みは2度行われているが、nullChop
を参照している、chopexec
は1度しか実行されていない。
ここで、「値が変化するたびにchopexec
のonValueChange
を実行する」という前提だけだと、pythonスクリプト上で複数回変化したとしても、CHOPExecute上では1度しか変化していないということになる。
結果:
python上からのネットワーク上のChop
値を変更している場合、chopexec
が実行されるタイミングはpythonスクリプトの実行が完了した後に1度だけ順次起こる。
Pythonスクリプト内からのchop読み書きとネットワーク接続の関係(破綻編)
上記の例からも、chopexec
の実行タイミングは、参照しているCHOP
のcookタイミングそのものではないことがわかります。よって、chopexec
等を用いて値を更新させている場合、例えば
constant1
からnull1
に受け取った値で、onChangeValue
時に隣のconstant_read
の値を更新する。
op('constant_read').par.value0 = val
この場合には、上記のようにconstant1
の値を更新して、その結果としてのconstant_read
の値を取得しようとすると、実行順番は下記のように、python実行後に、onChangeValue()
が実行されてconstant_read
の値が更新されるので、pythonスクリプト内でのwriteとreadの値は一致せず、readでは、”前回実行時の値”が取得されてしまう。
import time
import random
r = random.randint(1,10)
#write
op('constant' + str(1)).par.value0 = r
#read
v = op('constant_read')[0]
print('write ', r , 'read', v)
write 9 read 6.0 1576061048494.0022 ms
chop exec 1576061048494.885 ms
Pythonスクリプト内からのchop読み書きとCHOP Referenceでの参照接続
CHOP Reference
での参照接続はどうなっているのであろう。
結論:
ネットワーク接続と同じ。pythonスクリプトからの値取得時にcookされて、値が更新された状態で、constant_read
からの値を取得できる。
コンポーネント内の関数実行順序
システムが複雑になって来ると、container COMP
やbase COMP
へcustomize Component機能に使って、関数などをまとめていく事がありますが、COMP
の中にまとめた関数実行のタイミングがいまいち不明瞭でしたので、この勢いで実験してみました。
COMP
へのカスタムコンポーネント追加の方法については、他のよき記事を参照していただければ幸いです。
https://qiita.com/joe0hara/items/de0f91e8886c116504b3#_reference-00aa98459583b0bcec67
Extension Codeでの実行と、Custom Parameters でのPulseボタンへのpulse()
個人的によくやるのが、
- コンポーネントの
customize Component
でのExtension Codeを追加する -
Custom Parameters
でのPulse
ボタンを追加して、コンポーネントのパラメータリストから実行できるようにする- Parameterの
Pulse
ボタンに対して、btnName.pulse()
のように、スクリプトからPulse
ボタンを押す。
- Parameterの
の二種類ですが、どうやら後者の挙動が期待どおりにならないことが多かったので、調査しました。
text1 DAT
には下記のスクリプトを記載。
op('container1')
にまとめている関数、2種類の実行方法のタイミングを調べる。
import time
print('text1 python excuted 1 ', time.time() * 1000)
op('container1').PromotedFunction()
op('container1').par.Execute.pulse()
print('text1 python excuted 2 ', time.time() * 1000)
container1
内には、下記の2つ、Script
とparexec1
がある。Script
は、ComponentでのExtension Codeを追加して作成。parexec1
は、me.parent()
を対象にして、onPulse
関数で、container1
のPulse
ボタンが押されたときに実行するスクリプトを記述。それぞれ、実行時の時間も出力されるようにしておく。
Script text DAT
内スクリプト
from TDStoreTools import StorageManager
TDF = op.TDModules.mod.TDFunctions
import time
class Script:
"""
Script description
"""
def __init__(self, ownerComp):
# The component to which this extension is attached
self.ownerComp = ownerComp
def PromotedFunction(self):
print('PromotedFunction excuted ', time.time() * 1000)
parexec1
内スクリプト
import time
def onPulse(par):
print('container1 onPulse excuted ', time.time() * 1000)
return
結果のtextport
python >>>
text1 python excuted 1 1576063357049.925
PromotedFunction excuted 1576063357049.95
text1 python excuted 2 1576063357050.025
container1 onPulse excuted 1576063357050.614
これを、上記の実行したtext1DAT
のスクリプトを見比べると
##(再掲)
import time
print('text1 python excuted 1 ', time.time() * 1000)
op('container1').PromotedFunction()
op('container1').par.Execute.pulse()
print('text1 python excuted 2 ', time.time() * 1000)
Extension Codeによって実行した、container
内のScript
の関数は、pythonコード関数として期待したタイミングで実行されていますが、par.Execute.pulse()
から、parexec1
経由で実行したonPulse()
関数は、text1DAT
のスクリプトの完了後に実行されています。
Pythonスクリプトから、pulse()を2回以上実行した場合
ちなみに、ここで、text1DAT
のスクリプト内で、par.Execute.pulse()
を2度実行した場合には、、、
(これまでの流れから、なんとなく予感はしますが。。)
import time
print('text1 python excuted 1 ', time.time() * 1000)
op('container1').PromotedFunction()
op('container1').par.Execute.pulse()
op('container1').par.Execute.pulse() #2回目
print('text1 python excuted 2 ', time.time() * 1000)
結果:
python >>>
text1 python excuted 1 1576064625857.658
PromotedFunction excuted 1576064625857.682
text1 python excuted 2 1576064625857.71
container1 onPulse excuted 1576064625858.234
そうです。parexec1
のonPulse()
関数は1度しか呼ばれていません。(恐ろしや)
Extension Codeによる関数実行と、par.ButtonName.pulse()
による関数の実行は順序と性質が全く異なることがわかります。
#まとめ
今回は、日頃気になっていたCHOPExecuteの実行順番、pythonスクリプトとの関係、コンポーネント内の関数実行順序について調べてみました。
CHOP Executeの実行順番
- CHOPが並列に接続されている場合、ネットワーク生成順序の(若い方)新しく生成されたネットワーク順に、
CHOPExecute
が実行される - CHOPが直列に接続されている場合、ネットワークの終端に近い順に
CHOPExecute
が実行される - 同一のCHOPに対する複数のchopexec Datがある場合、参照リンクが若い方から
CHOPExecute
が実行される。
PythonスクリプトからのCHOP操作と、CHOP Executeの実行順番
- pythonスクリプトから、CHOPの値を取得するときにも、接続しているネットワークの処理(cook)が走る。
- よって、pythonスクリプトから、CHOPの値を操作してネットワーク経由で更新された値を即時取得することは可能
- 1.2.の性質はCHOP Referenceで接続していても同じ。
- しかし
CHOPEcute
は、実行しているpythonスクリプトが完了した後に実行されるので、実行順序に注意が必要
コンポーネント内の関数実行順序
- Componentの
extension code
のpythonスクリプトは、他のpythonから実行した場合、単純にpythonコードとして実行されるので、順序は期待どおり -
Custom Parameters
でのPulseボタンへのpulse()経由で行うと、実行中のpythonコード完了後に、実行される。かつ、複数回pulse()をよんでいても実行回数は1回になるので、注意が必要。
すこし、スッキリしましたね。これで、良い年を迎える準備ができそうです。