昨日の仮修正を破棄してバックアップに戻る

昨日の仮修正は一旦破棄してバックアップに戻ろう.何が問題なのか?一番の問題は先帝系列で明石中宮が展開されていないという点だろう.明石中宮は※3系列で外部との接点を持つ唯一のノードなので,このノード以外に系列優先ノードとなり得るカードは存在しない.このノードがリジェクトされているのは,対向する有効な実ノードが存在しないためだ.対向する有効な実ノードとは,すなわち先帝系列で展開されるべき明石中宮の人名枠に他ならない.なぜそういう流れになっているのか,追跡してみよう.

明石中宮の義母である紫の上の父式部卿宮は結婚を3つ持っている.単身婚,式部卿宮の北の方,紫の上の母だ.この式部卿宮+紫の上の母の結婚が展開されていない.この結婚枠の所属系列が未定であるためだ.これはMARGBOX::IsPrimeboxOrNoに「基準ノードの配偶者が外部に有配偶者婚を持っている場合には,その結婚は基準ノードの配偶者の配偶者側で展開される@20190128」というルールがあるためだ.このサンプルの基準ノードは#229 式部卿宮の北の方で,式部卿宮はその配偶者に当たるため,弾かれている.この規則を暫定的に止めて描画できた!

image

確かにかなり難しい図面であるかもしれない… しかし,このような反例があるために,このルールを破棄ないし緩和するというのはよい方向であるとは思えない.IsPrimeboxOrNoという関数はある結婚を夫と妻のどちらのポジションで展開するか?という疑問に応えるための関数で,基本にはどちらを選択しても描画は可能となっているという前提がある.この前提が崩れたのは,「基準ノードの配偶者が外部に有配偶者婚を持っている場合には,その結婚は基準ノードの配偶者の配偶者側で展開される」というルールが厳し過ぎるためというよりは,むしろ「もっと優先度の高いルールが存在する」ということだろう.

つまり,その結婚がある位置で展開されないと描画不能となるようなものが存在するということではないのか?しかし,それをルール化するのは容易くはない.系列枠リストの最初の7系列を表示してみた.

  1. #915 先祖=#701 式部卿宮の北の方(0) 優先=#701 式部卿宮の北の方(0) 始系列 type=始系列
  2. #917 先祖=#509 先帝(0) 優先=#1093 式部卿宮(1)→#701 式部卿宮の北の方(0) →主系列#915:式部卿宮の北の方 type=BTW左接続関係
  3. #919 先祖=#557 ※6(0) 優先=#1101 先帝の后宮(1)→#511 先帝の后宮(0) →主系列#917:先帝 type=婚姻関係
  4. #921 先祖=#405 ※3(0) 優先=#1102 明石中宮(1)→#339 明石中宮(0) →主系列#917:先帝 type=親元関係
  5. #923 先祖=#603 中務宮(0) 優先=#1104 明石の尼君(1)→#521 明石の尼君(0) →主系列#921:※3 type=婚姻関係
  6. #925 先祖=#637 右大臣(明石)(0) 優先=#1110 今上(1)→#339 明石中宮(0) →主系列#917:先帝 type=BTW左接続関係
  7. #927 先祖=#589 按察の大納言(若紫)(0) 優先=#1112 紫の上の母(1)→#535 紫の上の母(0) →主系列#917:先帝 type=婚姻関係

1番目の式部卿宮の北の方系列は始系列だから,無条件で描画される.2番目の先帝系列の優先仮ノードは式部卿宮であり,実ノードは基準ノードの配偶者だから,この系列自体には何の問題もない.もちろん,式部卿宮+紫の上の母の結婚を他系列に譲ったとしても,問題なく描画できる.問題は※3系列の優先ノード候補が明石中宮しかないという点にある.ここで躓くのは,式部卿宮+紫の上の母の結婚の移転先が,※3系列より系列順位的に後方にあるという点にある.このようなことは予想されていなかったので,この場で対処することは不可能だ.

解決策としては,上記で実行したように何らかの条件を提示してこの結婚が紫の上の母側ではなく,式部卿宮側で展開する必要があるということを示す必要がある.これを決定するためには少なくとも次のことが言えなくてはならない.

  1. 系列優先ノード候補を一つしか持たない系列Kが存在する
  2. この優先ノードの実ノードを(子どもとして)含む結婚はこの系列Kに先行して展開されなくてはならない
  3. この結婚を系列Kより後方の系列に移籍してはならない

判定1.もあまり簡単ではないが,2., 3. も分かり易いものではない.通常始系列に含まれる結婚は最初に展開され,基準ノードの配偶者の系列はそれに準ずると考えられるから,通常の手順であれば,この結婚が「後方」に移動することは避けられない.仮にこの手順に入るまでに,すでに系列枠リストは生成され,系列順位が決定しているとしよう.また,系列優先ノードも選定済みであるとする.実際,現行論理ではそうなっていると言ってよいと思われる.とすれば,上の条件文は以下のように書き換えることが可能かもしれない.

「IsPrimeboxOrNoの検査対象の結婚に系列優先ノード(候補)が(子どもとして)含まれる場合は本人位置で展開する」

例外としては,その配偶者の系列が「系列優先ノードの系列」より先行する系列である場合は移動を認めるとしてもよい.以上をまとめると,

「IsPrimeboxOrNoの検査対象の結婚に系列優先ノード候補が子どもとして含まれる場合は本人位置で展開する ただし,配偶者系列が系列優先ノード候補系列より上位にあるときはその限りではない」

となる.しかし,これは絵に描いた餅で終わる可能性もある.まだ,この時点では結婚枠はおろか,人名枠でさえどの系列に属するかは確定していないと考えられるからだ.系列枠はすでに生成済みであるとすれば,系列枠のPrimaryには系列優先ノードが入っているはずだから,系列枠リストを走査すれば確認することは可能だが,ややコスト高になる.CARDLINK自体にその情報(優先ノード候補であるという情報)が入っていれば探索コストはかなり削減できるのだが…

CARDLINKには先祖ノードへのリンクを格納するancestryというスロットがあるが,これは必ずしも所属系列とは一致しない.CARDLINKはNAMEBOXのリストを持っているので,このリストをチェックすることはできる.しかし,たとえば,式部卿宮+紫の上の母の結婚を検査するタイミングでは明石中宮のNAMEBOXはまだ一つも展開されていないのではないだろうか?実装してみよう.

いや,問題はもう一段難しい.IsPrimeboxOrNoの対象結婚枠は式部卿宮+紫の上の母だが,明石中宮はこの結婚枠には入っていない.

式部卿宮+紫の上の母→紫の上→明石中宮

という関係だ.そこまでここで追求するのは無理なのではないか?というより,もし,それをやるとすれば系列優先ノードから先祖ノードまでの経路上に存在するすべての結婚が検査対象となってしまう.実際,そこまでやらないと解決にはならないのではないかと思う.かなり厄介な話に発展してしまった.つまり,あるノードを系列優先ノードとして予約するためにはそのノードから先祖ノードまでのすべての結婚枠を予約する必要があるということになる.⇒この方向はかなり険しいが,もしそれしか道がないのだとしたらそうするしかない.

もう一つ別のより簡便な方式も考えられる.現行の論理で一度GoDownStreamを通した後,MakeUpTreeで再挑戦の機会を与えるという方式だ.これなら,確定できる系列はすべて連結完了しているはずだから,系列順位とは無関係に接続点を探すことができる.

現行方式でまさにそれをやっているのではないだろうか?TRIBEBOX:GetAlternativePrimeNodeという関数はそれを行うために用意されたのではないのか?であるとすれば,この関数をより洗練させればよいというだけの話になる.それは実現可能な話ではないかと思う.大分回り道してしまったが,ようやくスタート地点まで戻ってきた.

一応GetAlternativePrimeNodeで(不十分ながら)接続先を見つけることはできるようになったが,TRIBEBOX::DecidePrimaryNodeが通らない.この関数は自分自身を再帰的に呼び出しているので無限ループに陥ってしまう.結局まだ問題は解決していないということのようだ.GetRealnodeでは「空でない可視の人名枠オブジェクト」を求めているため,按察の大納言系列の#1112 明石中宮(1)が弾かれてしまっている.しかし,所属系列枠が決まっているのに不可視というのはどういうことだろう?⇒GoDownStreamで所属系列を設定するタイミングで可視化してみたが,状況は変わらない.

どこかで落とされているようだ.CARDLINK::NameBoxを実行すると初期化される.しかし,この動作はかなりおかしい.対象となっているのは明石中宮ではなく,北山の尼君だ.

CARDLINK::NameBox→ NAMEBOX::makeProxy→ initializeで生成された北山の尼君の仮ノードを隠蔽リストにつなぎ込んでいる.この措置は「挿入」として実施されるため,挿入位置のノードが操作されて対象ノードの後ろに移動し,このとき不可視設定されている.隠蔽リストは原則として不可視となっているため,このような動作になっているのだろう.GoDownStreamでは人名枠などの描画要素は生成されてはいるが,まだ可視のYリストには登録されず,MAKEUPTREEで初めて繋ぎ込まれるという動作になっている.このため,MAKEUPTREEで実施しているGetAlternativePrimeNodeは再チャレンジになっていない.

見てきたように,従来手順では優先ノードが決定できない系列が発生することは避けられない.これを予防するための措置を導入することは不可能ではないが,高度に難解なものになることは避けられず,時間・空間効率に及ぼす影響も無視できない.この意味では系列木の生成を段階化して,後段で最終解決するというのは現実的であると思われる.中間段階に当たるGODOWNSTREAMでは描画要素はすべて隠蔽リスト上にあり,かつ隠蔽リスト上にあるノードはつねに不可視とするというルールになっており,これを直ちに変更するというのも現実的ではない.

TRIBEBOX::GetRealnodeはMAKEUPTREEに入ってから適用される関数だが,MAKEUPTREEが完了するまでは描画要素がすべてYリストに繋がって可視化した状態にならないため,この関数で認める系列優先実ノードの条件を緩和するしかない.実際問題として,人名リンクが有効であるような人名枠は基本的に合法と考えられるから,ここでは可視/不可視を問わないことにする.これで描画まで進むことができた.ただし,一つだけ問題がある.

TRIBEBOX::GetAlternativePrimeNodeの中段,(T2 == major)のとき(marglink)で停止する.TRIBEBOX::GetAlternativePrimeNodeで系列枠#927 先祖:#588 按察の大納言 (若紫)の優先ノードを探すメインループを抜けたところで誤動作している.婚姻関係では見つけられず,逆婚姻関係を探して見つけているが,相手方が空であるために間違った分岐に進んでいる.相手方を見つける論理は以下のように単純なものだが,単身婚の場合があるということが見落とされていた.

if (wife == marg2->OTTO) partner = marg2->TUMA;
else partner = marg2->OTTO;

逆婚姻関係の場合には従系列で配偶者となっているのだから,主系列では本人となる可能性が高いと考えられるが,実際にどちらに割り当てられるかは必ずしも分明ではない.ある結婚の夫と妻のどちらを本人としどちらを配偶者とするかということは,その結婚枠をどの系列ないし,どの「家」に配置するかを決定することに等しい.その最終決定を行っているのがMAKEUPTREEフェーズなので,このプロセスが完了するまでは確定的なことは言えない.しかし,系列優先ノードの決定はそれらが確定する以前になされなくてはならないという矛盾がある.

系列優先仮ノード→実ノードという関係はある意味でかなりルーズな関係であり,必要なことは,①2つの系列関係が接点を持つこと,②同世代の2つの人名ノードの配置によって系列枠の垂直位置関係を決定すること,の2つが満たされれば十分であると考えられる.従って,その関係を決定するためにはそのノードが本人ノードであるか配偶者であるかまでは問う必要がないと(基本的には)考えられる.この立場からすれば,婚姻関係の実ノードはつねに(従系列における)本人ノード,逆婚姻関係の場合は(従系列における)配偶者ノードとしてよいのではないか?このルールなら単身婚の場合も問題なく処理できる(はずだ).

image

一応これでできたようだ.最初の図と今回の図では微妙に細部が異なる.最初の図では紫の上の母が夫の式部卿宮との接触距離まで接近しているのに対し,後の図ではかなり開いたものになっている.これは「基準ノードの配偶者が外部に有配偶者婚を持っている場合には,その結婚は基準ノードの配偶者の配偶者側で展開される」というルールを文字通り実現したもので,基準ノードの式部卿宮の北の方の眼からすれば,この方がずっと好ましいことは間違いないだろう.明石中宮も2番目の図ではかなり遠いところに配置されているが,式部卿宮の北の方の距離感からすれば,こんな感じなのではないだろうか?

このサンプルではTRIBEBOX::GetAlternativePrimeNodeの出動が必要となるケースは1件しか起きていない.それも,※3系列の明石中宮ではなく,按察の大納言(若紫)系列の優先ノードが紫の上の母から式部卿宮に切り替わるという動きだ.これはかなり予想外の結末だが,この障害の真因が,MARGBOX::IsPrimeboxOrNoで式部卿宮+紫の上の母の結婚を紫の上の母系列で展開するように決定したことにあることからすれば,当然の成り行きとも言える.この修正は,結婚枠展開の自由度を残したという点では「※3系列の明石中宮」にこだわるというアプローチより優れていると言ってよいと思う.

※3系列はどうなったかと言えば,明石中宮を優先ノードとして,按察の大納言系列の従系列になっている.しかし,この方式が「完璧」であると言えるかと言えば,多少の不安は残る.系列の従属関係が当初の線形順序を崩しているため,場合によっては,本来連結でなければならない系統図が複数のブロックに分離してしまう可能性はゼロではない.系列優先ノードの選択というのは,かなり難易度の高い部分だったような気もするが,ようやくある水準に達したのではないかという気もする.

TRIBEBOX::decidePrimaryNodeで系列優先ノード決定不能が起きる

▲BUG20-12-20 00-37-44.ZELを開いて,TRIBEBOX:decidePrimaryNodeで系列優先ノード決定不能が起きる.障害が起きているのは[系列枠]: #921 先祖=#405 ※3で優先ノード候補は#338 @4明石中宮だが,TRIBEBOX::GetRealnodeで有効な実ノードが選択できないため,リジェクトされている.

かなり難しい問題だ.ある意味で現行方式が破綻していると言っても過言ではない.ZTは当初実親子関係しか扱っていなかったが,養親子関係が導入された後でも基本的な構成規則である「すべての人名はいずれかの系列に所属するという」という原則が踏襲されてきた.養親子関係をサポートするためにはこの原則を緩和するしかないのだが,原則それ自体の見直しが行われることがなかったため,不良が潜伏していつか露見するという宿命を負っていたのではないだろうか?

現行ではたとえば,CARDLINK::ancetryには所属系列の先祖ノードへの参照が格納されるようになっている.これは,つまり,ある系列に所属する人名は他の系列に所属することはできないことを暗に示している.実際には,養親子関係を含む系図を扱っている以上,運用的にそれをカバーしてきたものと思われるが,原理的に整合していないのでどこかで破綻することは避けられない.どこをどう改訂ないし改善すればよいのか?TOPOLOGY::topologicalSortをフェーズで区分すると,

  1. TOPOLOGICALSORT 系統並び替え開始
  2. CLEARTABL テーブル初期化
  3. EXTRACTPARTIAL 部分図ノードの抽出
  4. SETBASECARD 基準ノード設定
  5. KINSHIPDEGREE 予備検定(親等計算)
  6. DECOMPOSITION 本検定(系列分解)
  7. PHYLETICTREE 系列枠の生成と世代計算
  8. GODOWNSTREAM 系図木生成と展開:グループ゚を設定
  9. MAKEUPTREE 系図木構築:Yリストへの繋ぎ込み
  10. SETSIBLINGS 拡張子ども枠の設定
  11. SETRELATIVE 系図木相対領域計算中
  12. BUILDGENELIST 基本/系列世代枠リストの生成
  13. SOLVECOLLISION 系列内同世代結婚枠の衝突解消処理
  14. BUILDCENTERLINE 血統軸線図の構築
  15. TRIBERELOCATION 系列枠再配置
  16. SAMEGENEMARRIAGE 重婚同類グラフを生成
  17. AFTERMARGSAMEGENE 重婚同類グラフの後処理
  18. SYMMETRICGEOMETRY シンメトリ婚を展開する
  19. MAINEXPERIMENT 系列間衝突回避処理
  20. HORIZONTALORDER 先祖並び自動オフ時の系列水平配置
  21. FOLDINGCHANNELS チャンネル数自動のときのチャンネル整理
  22. HEAPTRIBEBOX 系列枠包括領域集積
  23. MAKEABSOLUTE 絶対座標系変換
  24. SETTITLEBOX 系図外枠矩形領域を最終決定
  25. DRAWSTAGE 系図木描画準備完了

のようになる.ただし,最後のDRAWSTAGEはCOUPLING:TopologicalSortで設定される.いま問題になっているところは(6)DECOMPOSITION から(9)MAKEUPTREEまでの処理と考えられるので,そこだけを抽出すると,

  1. DECOMPOSITION 本検定(系列分解)
  2. PHYLETICTREE 系列枠の生成と世代計算
  3. GODOWNSTREAM 系図木生成と展開:グループ゚を設定
  4. MAKEUPTREE 系図木構築:Yリストへの繋ぎ込み

のようになる.(1)DECOMPOSITIONではすべてのノード(人名リンク)に先祖ノードを割り当てて,排他的な系列に分解する.(2)では系列枠オブジェクトを生成して,系列枠リストを構成する.(3)GODOWNSTREAMでは各系列の先祖ノードからの下流検定を実施して,人名枠オブジェクト(仮ノード)を生成し,系列の最小・最大世代番号を決定する,(4)MAKEUPTREEでは系図木(描画リスト)を生成し,すべての描画要素の初期化を行う.エラーが出ているのは(4)のMAKEUPTREEだ.

フェーズの整理を行っていて,エラーが出た.上のリストで18. SYMMETRICGEOMETRY シンメトリ婚を展開するを実行している場所は実際には,21. FOLDINGCHANNELS チャンネル数自動のときのチャンネル整理の直前に移動しているので,SYMMETRICGEOMETRYの値を変更したところ,フローはまったく変化していないも関わらず,エラーが出るようになった.COUPLING::TopologicalSortの出口検査で(SymmetryList && SymmetryList->len() != SYM) で停止している.

SYMMETRICGEOMETRYの値が変化したことより,TOPOLOGY:BuildSymmetryListの直前でPHASEにSYMMETRICGEOMETRYをセットしていることが障害の原因となっている.BuildSymmetryListの中でフェーズによって処理が分岐するようになっているのではないか?⇒いや,BuildSymmetryListの後でCheckMultiCardsを実行してもエラーにはならない.BuildSymmetryListでフェーズをセットしなければエラーは回避できるが,なぜこのようなエラーが出るのかについては追求する必要がある.⇒原因は大体つかめた.

3つパターンがある.①オリジナルパターン→エラーなし,②BuildSymmetryList値を変更のみ→エラーなし③SYMMETRICGEOMETRY値を変更,BuildSymmetryList前でフェーズ設定→エラー発生.エラーはTOPOLOGY::SymmetryListのカウントと系列枠にセットされたカウントの合計が一致しないというものだが,実際にはTOPOLOGY::SymmetryListのカウントゼロで系列枠のシンメトリ婚カウント1という状況だ.

①のオリジナルコードの場合には,BuildSymmetryListの実行時のフェーズがSYMMETRICGEOMETRYと異なるため,MARGBOX:getSymmetricLeftBoxで厳格な条件が要求されるためシンメトリ婚不成立となっている.また,②の場合は,BuildSymmetryList実行時のフェーズ番号がSYMMETRICGEOMETRYよりも小さくて無動作で抜けているのでやはりシンメトリ婚不成立となり,不一致は生じない.③の場合はBuildSymmetryListの実行時フェーズがSYMMETRICGEOMETRYと一致するため条件が緩和されて,シンメトリ婚が成立,おそらく後に破棄されたためカウント不一致になるのだろう.

構成規則が時と場合によって厳しくなったり緩和されたりするという仕様はかなり問題があるので統一した方がよいと考えるが,カウント不一致というのはまた別の問題なので,まず先にこちらをクリアしておこう.⇒対処した.⇒今度は系列枠のSymmetryCountの方が少なくなってしまった.Ancesry.zelを開いて不一致が発生する.SymmetryListというのはMARGBOXを要素とするリストだ.シンメトリ婚に設定されると,その結婚枠はSYMMETRICMARGという属性を持つようになる.

SYMMETRICMARGがセットされるのはBuildSymmetryListの中だけで,このときは系列枠のSymmetryCountも同時にインクリメントされている.また,この値がリセットされるのはMARGBOX:AbandonSymmetricの中だけで,今回の修正はこの関数に関わるものだ.SymmetryCountのデクリメントはresetghostbits(SYMMETRICMARG)で実行される.resetghostbitsを呼ばずに直接リストからパージして,SymmetryCountをデクリメントしている箇所もあったが,すべてresetghostbitsで操作するように修正した.

ただし,resetghostbitsの中の論理にもミスがあった.SymmetryList->Removeの戻り値を見てカウントダウンしていた.LIST::Removeは現在リスト長を返すような仕様になっている.Ancestry.zelでシンメトリが崩れているという現象は大分前から出ているが,落ち着いてから調べることにする.最初の問題に戻ろう.

▲MARGBOX:getSymmetricLeftBoxでSYMMETRICGEOMETRYのときに限ってシンメトリ婚成立の条件を緩和している

▲Ancestry.zelでシンメトリが崩れている

DECOMPOSITIONではまだ描画オブジェクトは生成されていないので,人名リンクベースで人名をグループ分けしたものを系列と称しているに過ぎない.PHYLETICTREEでは先祖ノードを頂点とする系列枠オブジェクトが具体的に生成される.次のGODOWNSTREAMでは先祖ノードの下流系を展開してメンバーとその配偶者の人名枠を生成し,その所属を決定する.最後のMAKEUPTREEでは系列優先ノードを(最終)決定し,系列の相互位置関係を確定する.

見た限りではどうも三番目のGODOWNSTREAMに問題があるように思われる.ここでは,所属系列の移動ということがかなり頻繁に起こっているため,どこかに盲点が発生しそうな感触がある.

NAMEBOX #339 明石中宮(0)はPHYLETICTREEフェーズで[系列枠]: #917 先祖=#509 先帝(0)に振り分けられているが,GODOWNSTREAMに入って,※3系列に移動している.先帝系列は系列枠リスト上では※3系列より先にあるのに,明石中宮の人名リンクが先帝系列で展開されないという理由が分からない.このサンプルには系列が89個含まれているが,最初の7つまで表示すると,

  1. #915 先祖=#701 式部卿宮の北の方(0) 優先=式部卿宮の北の方 始系列 type=始系列
  2. #917 先祖=#509 先帝(0) 優先=式部卿宮  type=婚姻関係
  3. #919 先祖=#557 ※6(0) 優先=先帝の后宮  type=婚姻関係
  4. #921 先祖=#405 ※3(0) 優先=明石中宮  type=親元関係
  5. #923 先祖=#603 中務宮(0) 優先=明石の尼君  type=婚姻関係
  6. #925 先祖=#637 右大臣(明石)(0) 優先=髭黒  type=婚姻関係
  7. #927 先祖=#589 按察の大納言(若紫)(0) 優先=紫の上の母  type=婚姻関係

のようになっている.※3系列は順位からいくと4番目だ.

系列優先仮ノードが逆婚姻関係でパートナーが先祖ノードのケース

TRASHCANとUNDOSYSTEMというZTシステムの重要な機能のモジュール化に成功した.TRASHCANは完全なネィティブnoduleクラスとして確立され,UNDOSYSTEMもその基幹部分をUNDOBASEというネイティブクラスとして切り出すことができた.これでシステムの不透明な部分が相当程度グラスボックス化したと言える.時期は不明だが,前に一度試みて不成功に終わったUNDO機能と関わりがあるNODULE::numberという変数も廃止することができた.アプリ終了時には,ゴミ箱の内容を廃棄してから delete CAN を実行しているが,これもストレートに delete CAN だけで済むことを確認した.しかし,delete CAN でゴミ箱の中身が消えるのはなぜだろう?CleanSlotではすべてのスロットをdeleteしているが,スロットゼロは除外されているはずなのだが…

下図のような大量のカードを一括削除して,TRIBEBOX::SetPrimeLinkで停止した.

image

(prmtype && !marglink && PHASE <= TRIBERELOCATION && CENTERLINE != CENTERLINE_SEXIAL)という理由だ.始系列ではないのに優先ノードが結婚リンクを持っていない.⇒BUG20-12-19 17-32-21.zelで反例サンプルを保全した.この障害は2020/12/02のリリース版でも発現するので,最近の修正とは無関係と思われる.

TRIBEBOX::GetAlternativePrimeNodeの論理に穴が空いていた.先祖ノード#238 FAMILYTREEの対象系列#534は優先仮ノード#166 extraslot2の相手方を見つけられなかったため,代替として#226 COUPLINGを選択した.相手方#154 couplingは系列#528 先祖=#426 couplingの本人(先祖)ノードだが,既存コードには相手方が先祖の場合が抜けていた.優先仮ノードの結婚リンクは,すでにパートナーを見つけた時点で決定可能なので,既存コードは不要と思われる.

源氏物語6のテーマを切り替えようとして,「フェーズのイレギュラーな遷移」エラーが発生した.DRAWSTAGEからTOPOLOGICALSORTに移行しようとしている.テーマを切り替えただけではトポロジーには変化はないが,図形のサイズ・位置には変化が現れるのでレイアウトの再計算を実施する必要がある.系統並び替えを実施しないで再描画するUpdateDiagramという関数はあるが,これで間に合うだろうか?spannodcount > 0 ということは系統並び替えが必要ということを意味するのだが… TOPOLOGY::UpdateDiagramでは「写真を表示しているときは調整不能,系統並び替えが必要」としている.⇒ここでは,FAMILYTREE::updatespannodで強制的にフェーズをINITIALIZEDに落とすということにしておこう.

ZTシステム構成図7.ZELを開いた後,BUG20-12-19 17-32-21.ZELを開こうとしてエラーになった.COUPLING::initializeの出口で(TemplateBuff)が起きている.⇒再現しない.⇒再現した.源氏6の後,ZTシステム構成図7.ZELを開いて起きた.ただし,再現するには何か条件が必要なようだ.TemplateBuffは記録ページテンプレートを保持するためのバッファで,COUPLING::SaveTemplateで使われている.しかし,この関数は呼び出されている形跡がない.⇒いや,VBのSaveFileからの呼び出しがある.

SaveTemplateだけでなく,LoadTemplateというのもある.「記録ページテンプレート」というのは過去の遺物だ.以前は記録ページのレコードをカテゴライズするためのテンプレートファイルというのを持っていたが,いまは完全に仕様から落とされている.TemplateBuffは廃止でよいと思われるが,エラーが発生する状況をもう少し詳しく確認しておきたい.ひょっとしたら,バッファオーバーランのようなことが起きている可能性もある.SaveTemplateはファイル保存時に実行されているので,ファイルを保存してみれば分かるだろう.

確かに実行されているが,サイズはゼロだ.ただし,COUPLING:SaveTemplate側ではサイズ+4の領域を確保しているので,サイズゼロでもメモリブロックの取得は実行される.記録ページテンプレートはCOUPLING::SerializeHeaderで読み込まれている.この部分の論理は残しておかないと古いファイルが読み込み不能になる可能性がある.また,現状では書き込みの部分も作動しているはずだから,この論理を止めるためには「REVISIONのアップデート」が必要だ.⇒片付いた.REVISIONを更新したので,ここで一度リリース版を起こしておこう.Version 2.2.0.019 Release 2020-12-20とした.

▲源氏6.1の318点から48点を一括削除してTRIBEBOX::SetPrimeLinkでエラーが発生した.BUG20-12-20 00-37-44.ZELで再現できる.問題の系列は先祖#404 ※3の系列枠#921で,本来の優先ノードは#338 明石中宮だったのだが,このノードが系列所属ノードの中に入ってこない.どこかでancestryリンクを書き換えてしまっているのだろうか?元々は先帝系列に属していたようだ.ancestry値はTOPOLOGY:FilteringKinship→ GetkinshipDegree→ …→ CARDLINK:getKinshipDegreeで設定されているが,TribeDecompositionの入口で一度リセットされ,→…CARDLINK::KinshipDegreeで再設定される.

どうも現行方式の原理的欠陥が露呈してしまっているような気がする.現行方式では一度設定された先祖リンクは変更できないようになっているが,場合によっては不可避の系列優先ノードというのが存在する.つまり,外部に接点を持つカードがそれしかない場合には,そのノードは複数の系列にダブル登録するしかないのではないか?おそらく「逆婚姻関係」というのはそのような矛盾を解消するための苦肉の策だったのだろう.しかし,「逆婚姻関係」はある限られた範囲でしか成立しない.そもそもMakeUpTreeの段階になって,GetAlternativePrimeNodeを実行しなくてはならないというところがこの弱点の現れだ.

系列の連結関係を「婚姻関係」に限定すれば,現行方式でもカバーできているのかもしれない.婚姻関係なら一方を系列に属する本人,他方を配偶者として連結することができる.しかし,「親元関係」ではそういう訳にはゆかない.どちらも「本人ノード」として立てなければならないからだ.逆に言えば,そのような欠陥があるにも関わらず,部外の明石中宮が優先ノードとして選択されているということは抜け道はあるということだろう.なぜこの選択がリジェクトされることになったのかその理由を見てみよう.

TRIBEBOX::GetRealnodeで弾かれているためだ.明石中宮は仮ノードを2つ持っている.#339 明石中宮(0)が※3系列,#1112 明石中宮(1)は按察の大納言(若紫)系列だが,(1)はIsSolidNameBoxで弾かれている.これは,按察の大納言(若紫)系列が※3系列より後方にあるためと思われる.明石中宮は先帝系列に属しているのに,それに属する人名枠が存在しないのはなぜだろう?多分,これが一番の問題点なのではないかと思う.⇒.#339 明石中宮(0)は最初に先帝系列に編入されている.TRIBELIST::GoDownStreamで※3に切り替わっている.

昨日朝イチで片付けた障害がぶり返している

アプリを起動して,カードを1枚削除→終了でGetCardBaseのエラー(PHASE < DRAWSTAGE)が起きるという,昨日の朝イチで片付けた障害がぶり返している.ただし,今回はUNDOシステムありでテストしているので,昨日の朝とは条件が異なる.系統並び替えが実行されていないとすれば,昨日のUNDOの改修が影響しているのだろう.⇒いや,UNDOSYSTEM::CommandEndからCOUPLING:TopologicalSortを実行している.少なくともTopologicalSortの出口ではDRAWSTAGEになっている.⇒確かにどこかでフェーズをINITIALIZEDまで落としている.

心当たりはある.おそらく,FAMILYTREE::SetUndoBaseだ.これはUNDOCOMMANDに基準ノードなどの情報を設定する関数で,これまではあちこちに分散していたのを関数化したものだ.この中でSetPhase(CHAOTICSTATE)を実行している.この関数は,①UNDOSYSTEM:CommandStart,②UNDOSYSTEM:MakeNewCommand,③UNDOSYSTEM:CommandEndから呼び出されている.最後の③は系統並び替えの完了後だから,ここではSetPhaseは実行するべきではない.多分これはCommandStartFlagで切り分けることができるだろう.

カード削除2件→UNDOでCARDLINK::DownStreamのエラー(Invalidated || !nambox || !nambox->YUpper() || !nambox->getY() || !tribe)が起きた.namboxが空.かなりまずい.起動→基準カード削除→UNDOでは別のエラーになる.TRIBELIST:MakeTribeBoxで(marglink && !marglink->GetAncestor())というエラーが起きている.最初のエラーの再現手順を確定しておこう.

ZTシステム構成図7.ZEL:全体図を#1 couplingで開いた後,①#1 couplingを削除,②#61 NLIST < LISTNODE, CID>, #87 GENEBOX, #230 GENEBOXを選択して一括削除,③UNDO という手順で再現できる.手順が簡単なので,2番目の障害から追いかけてみよう.UNDOSYSTEM::UndoRedoCommandでUndoProcessを実行後の系統並び替えを実行しているところだ.⇒いや,事例としては最初の方が易しい.namboxが空ということはUNDOで復元に失敗したというだけだが,2番目の事例では状況を解析するのがかなり厄介だ.再現手順でも,最初のカード削除は省略できる.

障害が起きているのはCARDLINK:#369 @61NLIST< LISTNODE, CID>だ.確かにnamboxが空になっている.UNDOでは通常系統並び替えが実施されるため,描画要素は保全の対象になっていない.しかし,人名リンクには必ずデフォルトで1つは人名枠が付属することになっている.これをどこかで補充しなくてはならない.UndoRedoCommandとUndoProcessはほとんど外部依存コードなので従来論理がそのまま実行されているはずなのだが…

現行ではOpenFamilyBaseの中でKAKEIZU::readFamilyBaseを実行後にInitLinkTable→CARDLINK::initializeでデフォルトのNAMEBOXを生成している.描画要素はすべて頂点のTREEVIEWに描画リストによって連結されている.INITIALIZEDの段階では描画リストは一応使える状態になっていることが予定されている.NAMEBOXはCARDLINKに接続しているので,CARDLINKが削除されたとき同時に削除されているから,復元しただけではnamboxは元の状態には戻らない.

昨日のログでは「RestoreShadow オーバーライドは不要」としているが,間違っている.この関数は元々UNDONODEが実行していたものだが,UNDOSYSTEMに移管した後,外部依存コードなしと認定されてUNDOBASEが受け持つことになったものだ.明らかにオリジナルのコードには外部依存コードが含まれている.⇒UNDOSYSTEM:RestoreShadowを復元して動作するようになった.⇒2番目の障害も解消し,完全に動作するようになった.

バックアップも取ったので,ここまでの修正をフィックスしておこう.

  1. UNDOからTITLEINFOを切り離す@20201217 8箇所
  2. UNDOを基本・拡張クラスに分解@20201217 1箇所
  3. RestoreShadowをUNDOSYSTEMに移管@20201217 6箇所
  4. TRASHCANのアプリ依存度を確認する@20201216 2箇所
  5. GetCardBaseの動作にフェーズは関わりがない@20201217

DEFINETRASHCANとDEFINEUNDOSYSTEMの2つのオプションの組み合わせ,つまりゴミ箱を使う/使わないxUNDOを使う/使わないの4つのパターンすべての動作を確認したが,問題なさそうだ.

UNDO機能なしで動作することを検証する

UNDO機能なしの動作を検証するために,UndoRedo.h内のすべてのクラス定義を止めてビルドしたバージョンをテストしているところだが,実行時にFAMILYTREE:GetCardBaseで(PHASE < DRAWSTAGE)エラーが発生する.これは系統並び替えが実施されていないことを意味する.UNDOではサポートするすべてのコマンドの入口と出口でUNDOSYSTEM:CommandStartとCommandEndという関数を呼び出しているが,系図木に変化があった場合にはCommandEndで必ず系統並び替えを実行している.フェーズがDRAWSTAGEになっているということは,系統並び替えが実施されたことと同義だ.

GetCardBaseは,アプリの人名カード画面に表示するためのデータを取り出す関数で,この処理はフェーズに関わりなく実行可能なはずであるから,まず,ここでは停止しないようにしておこう.⇒今度は,TREEVIEW::GetScrollValueで同じエラーが発生する.GetScrollValueはアプリ側のCenteringCardSubから呼び出されている.ここでは図面のセンタリングを実行しようとしているのだから,その前に図面がレンダリングされている必要がある.⇒やや変則的だが,GetScrollValueから直接系統並び替えを実行できるようにしておこう.⇒これで問題は解決した.UI的にはUNDOシステムが搭載されていないので,カードを削除しても系図画面のツールバーのUNDOボタンは有効にならない.

これでUNDOとゴミ箱なしでもシステムが問題なく動作することを確認できた.TRASHCANはアプリケーションコードにまったく依存しないネイティブなnoduleクラスとして確立されたが,UNDOにそこまでのことを要求するのは無理がある.実際,UNDOを機能させるためにはアプリケーションコードのあちこちにUNDOSYSTEMのコードを埋め込む必要がある.(コードと言っても少数の特定関数を呼び出すだけだが…)問題はUNDOSYSTEM自体が(どの程度)アプリケーションに依存しているかという点だ.まず,この点を確認してみよう.

UNDOCOMMAND(a.k.a. UNDOCHAIN)にはもろにCARDLINKへの参照が4個設置されている.これについては後で考えるとして,暫定的にvoid*としておこう.⇒「暫定:UNDOSYSTEMの外部依存度@20201217」というマクロでコンパイルエラーを止めてビルドは通るようになったが,もちろんこのコードでは実行できない.「暫定:UNDOSYSTEMの外部依存度@20201217」は45箇所も入っている.満身創痍というところだ.今回はこれを一掃するところまでは目論んでいないのだが,何が可能か?考えてみよう.高度に抽象化されたUNDOシステムを想定することは可能だが,今日明日の課題ではない.

アプリケーションとUNDOシステムを切り離すための速攻的な措置を考えるとすると,アプリとUNDOの間にヘルパーないしサポートクラスのようなものを入れるということがまず考えられる.UNDOSYSTEMクラスを基本クラスと拡張クラスに分解し,基本クラスを完全にアプリから切り離すということも考えられる.これを実装するのはそれほど難しくないのでやってみることにしよう.ただし,その前に基本クラスや基本関数自体がアプリ依存になっているという問題がある.たとえば,UNDONODEクラスには最初からCARDLINKへの参照が入っているし,UndoProcessというUNDO処理関数まで余分な情報を必要としている.

int UndoProcess(bool redo, long &basenode, long &primary, short &lastcommand, TITLEINFO &titleinfo);

UNDONODEに入っているCARDLINK*はそれぞれ,①主選択カード,②現基準カード,③全体図基準カード,④部分図基準カードへの参照だ.系統並び替えなどを実施すると基準カードが変化するが,系統並び替えなどの操作はUNDOの保全対象となっていない.(その代わり系統並び替え履歴,主選択カード履歴で巻き戻しすることはできる)これらの情報は本来UNDOの守備範囲外なのだが,UNDO/REDOで再描画したときの画面を元の状態に近いものとして表示するためにはどうしてもこれらの情報を時系列で保全しておく必要がある.

それにしても,UNDONODEのような構成要素にその情報を持たせるというのは適切とは言えないが,便宜上このような仕様になってしまった.それどころか,UndoProcessの最後の引数であるTITLEINFOはアプリに現在の部分図タイトル情報を渡すという目的のためだけに設置されているようで,もしそれが本当ならかなりの手抜きというか,横着としか言いようがない.まず,この点を確認してみよう.

確かにそうなっている.TITLEINFOは下り方向でしか使われていない.宅急便の戻り脚に発送を依頼するというのはありとしても,これではまるきり,郵便屋さんに宅急便の荷物を預けるようなものだ… UndoRedoCommandを実行しているのはGCのmZelkova:mUndoRedoで,取り出したTITLEINFOはLastTitleというところに格納されている.TITLEINFOの取り出し関数ないしコマンドが見当たらない.TITLEINFORMATIONを取り出す関数はあるが,TITLEINFOとは内容が異なる.すべてのTITLEINFOを取り出す関数と現部分図エントリを取り出す関数を組み合わせれば目的は達成できそうだが,ここでは放置して動作を観察してみることにする.

▲mZelkova::mUndoRedoでTITLEINFOを更新する手段を導入する

UNDOCOMMANDに入っているCARDLINK情報は現状のままとして,UNDOSYSTEMを2階層に分解するというのをやってみよう.外部からはUNDOSYSTEMとして認識されているので,この名前を拡張クラス名として維持,基本クラスはUNDOBASEとしてみる.⇒実装した.

UNDONODE::RestoreShadowでLINKTABLEを参照している.これを避けるためにこの関数をUNDOSYSTEMに移管してみる.UNDONODE:UndoRestoreもUNDOSYSTEMに移管する.⇒一応動作しているようなのでバックアップを取っておこう.UNDOシステムにはUNDONODE,UNDOCOMMAND,UNDOBASE,UNDOSYSTEMの4つのコンポーネントがあるが,外部依存コードはUNDOSYSTEMが単独で扱うようになった.UNDOCOMMANDが持っていた4つのCARDLINK*はすべてlong整数に変えてカードの参照番号を格納するようにした.

UNDOSYSTEMのコードを少し整理してみよう.現在UNDOBASEは17個関数を持っているが,そのうちの6個はUNDOBASEで完結してUNDOSYSTEMでオーバーライドされないものだ.それ以外のものも,UNDOBASEの関数でカバーできるところはできるだけUNDOBASEの関数を呼び出すようにして,UNDOSYSTEMには外部依存部分だけが記述されるという体裁にしたい.

  1. BackupPointData 一部にはネイティブコードもあるが,ほとんどまるごと外部依存コード
  2. CommandStart UNDOBASEのコード+FAMILYTREE:SetUndoBase BackupPointDataは仮想関数とした
  3. UndoProcess ほとんど外部依存コード
  4. RestoreShadow オーバーライドは不要
  5. GetUndoStat UndoCurptr不在のときは,FAMILYTREE:SetUndoStat
  6. UndoRedoCommand ほとんど外部依存コード
  7. UNDOSYSTEM::CommandEnd UNDOBASE+外部依存コード

賽の河原で成仏しきれない亡者9名を発見

ゴミ箱の廃棄中メモリブロックカウントの不整合が発生する.メモリブロックカウントはgetmemでヒープからメモリブロックを取得したときにインクリメントされ,delmemで解放されたときにデクリメントされる.getmemはGlobalAllocPtrを呼び出してグローバルメモリを取得し,delmemはGlobalFreePtrでそれを返却する.これら2つの関数以外にGlobalAllocPtrとGlobalFreePtrを使っている場所は存在しない.つまり,入口と出口は完全に押さえられている.にも関わらず,何度数え直しても記録されたカウントと現物個数が一致しない.

nodule::operator deleteでは通常はdelmemを実行する代わりに,そのオブジェクトをゴミ箱に送付する.ゴミ箱が存在しない場合にはどうなるのか?ZTシステムは必ずしもゴミ箱を必要としない.ゴミ箱を設置しないというオプションでも動作するというのが設計上の要件だ.確かに,ゴミ箱廃棄中というのはかなりクリティカルな状況であることは間違いないが,それにしてもこの不一致はどこから生じているのか?

成仏しきれない仏がいるとすれば,それは賽の河原であるに違いない.そこを探してみるしかない.しかし,どうやって?いや,それは至って簡単だ.ThrowTrashCanの中でdeleteを実行している行の前後を監視して,メモリブロックカウントの推移を見ればよい.もし,deleteを通過しているにも関わらず,カウントが変化していないとすれば,そのオブジェクトが探している失踪者であることは間違いない.これで成仏しきれない9名の亡者を特定することができた.あとはトレースするだけだ.

これらの失踪者はアプリ終了時,FAMILYTREE::EraseFamilyTree→ LINKTABLE::ClearTableによって削除されているが,Shadowを持っているためゴミ箱には直行せず,Nringに一時係留された後resetUndoChainでUNDOチェーンがリセットされるタイミングでゴミ箱に移動している.UNDOチェーンのリセットではチェーンに連結されたすべてのUNDONODEが削除されるが,このUNDONODEのデストラクタ→UNDONODE::Disposeで実施している操作に問題がある.

UNDONODE::Disposeはそのノード(UNDONODE)が参照している実ノードのDELETED値をDEADに設定してからゴミ箱に送付しているが,その実ノードがShadowを持っているため,LIFE値はそのままとしている.この操作は,「Shadowを持つオブジェクトは削除してはならない」というUNDO規則による.ゴミ箱が廃棄されたときにこのオブジェクトは閻魔大王(NODULE::operator delete)の前に進み,「LIFEはセットかリセットか?」という大王の尋問に「セット」と答えるため,大王は「帰れ」と宣告し亡者は放免されて賽の河原に戻るという筋書きだ.

この筋書きが正しいとすれば,一つの簡単な解決法として「NODULE:operator deleteでは無条件にdelmemする」という方法がある.多分この解法はそれ自体としては誤っていないと思われるので,実装してみることにしよう.⇒問題なさそうだ.エラーはこれで解消されたが,UNDONODE::Disposeの扱いには問題があるので対策を講じる必要がある.⇒対処した.「Nringとゴミ箱の重複登録を禁止@20201213」という指針に従って,実ノードがShadowを持つ場合にはゴミ箱に入れないようにした.ゴミ箱を廃棄するときには,すべてのUNDONODEが削除されるので,その実ノードのShadowチェーンの最後のUNDONODEが削除されたタイミングでゴミ箱に入ることになる.

修正が大分累積してしまったので,まとめて一掃しておこう.12月10日以降の修正としては以下がある.

  1. Clean仮想関数の導入@20201210 7箇所
  2. METRIXでTREEVIEWを参照しない@20201210 1箇所
  3. 参照リンクを移動してはならない@20201210 3箇所
  4. CHAOTICSTATEでは描画要素を無視@20201211 1箇所
  5. METRIXからのNAMEBOX参照を廃止@20201211 16箇所
  6. metrixからのTREEVIEW参照を廃止@20201211 16箇所
  7. Nringとゴミ箱の重複登録を禁止@20201213 19箇所
  8. BUNSFILE:closeでバッファを解放@20201213 1箇所
  9. ENDOFAPPLICATIONではなくGROUNDZEROで終わる@20201211 2箇所
  10. countゼロの参照リストを削除する@20201214 1箇所
  11. BUNSBUFを廃止する@20201214 8箇所
  12. delmem,_delmem,delmem_を使う@20201215 2箇所
  13. NODULE:operator:deleteでは無条件にdelmemする@20201216 1箇所

この他以下のオプションがOFFになっている.

  1. COMMONHEADERSHORTの改訂@20201210
  2. Disposeの設置を義務化@20201211
  3. ゴミ箱なしの動作を確認@20201214
  4. NODULE:operator:deleteを廃止する@20201214
  5. DoublePtrで親リンク空の場合NULLSPOTを返さない@20201215

このうち,(3)のみ保留とし,それ以外はすべて廃止する.この他仮修正が11箇所ある.⇒すべてクリアした.

ZTシステム構成図7.ZELの全体図を#1 couplingで開いて,このカードを削除→UNDOでUNDOSYSTEM::UndoProcessのエラーが発生する.UNDONODEの参照する実ノードの親リンク空というエラーだ.⇒これはあり得る状態だ.この実ノードは削除され,フロート状態でNring上にある.つまり,これは「Nringとゴミ箱の重複登録を禁止」によって起きるようになった事象で,状態としてはノーマルだ.

同上操作で,FAMILYTREE::GetCardBase中(k >= hubo->margbase.kids)で停止した.huboのkids配列にカードが存在しないというエラーだ.このカードは#5 pagesetupで削除されたcouplingに代わる基準ノードで,5人兄弟の2番目.カード削除前は,coupling+COUPLINGの子どもで4人兄弟の先頭だった.母親のCOUPLINGが単身になり,単身婚の子ども1人を加えて5人兄弟になっている.

UNDOで戻ったときに親の結婚ページで兄弟4人というのは正しいが,リンクがすべて空ではどうしようもない.⇒この結婚リンクは夫が削除されたために妻の単身婚と併合され,削除されているはずなのにUNDOで<再生>されていない.⇒UNDOシステムでIsInTrashCanという関数が4箇所で使われている.UNDOはゴミ箱の存否と関わりなく動作しなくてはならないのだから,IsInTrashCanを使うことは許されない.⇒代わりに(Deleted() != EXISTING)で判定するようにした.⇒この修正だけで完全に元通り動作するようになった.

「ゴミ箱なしの動作を確認@20201214」オプションを復活させて動作を確認してみよう.今回は少し厳格に,TRASHCANのクラス定義それ自体を止めてみる.⇒確立できた.「DEFINETRASHCAN」というマクロで切り分けた.このオプションをSPECIFICATIONとすれば,「ゴミ箱なしの動作を確認@20201214」は廃止してもよいだろう.元々あった「USERECYCLESYSTEM」も廃止でよいと思う.このオプションでは「描画オブジェクトのリサイクルシステム」ということが意識されているようだが,現在のリサイクルシステムは基本的にZTのすべてのオブジェクトクラスをカバーするものとなっている.

TRASHCANのコードがどの程度アプリケーションに依存しているか,あるいはどの程度までアプリケーションから独立しているかを見てみよう.これを調べるにはnodule.hだけをインクルードしてみればよい.⇒TRASHCAN::ReuseWasteではCOMPLISTとCARDLINKの情報が必要となっている.しかし,これは多分不要だと思う.これらのクラスのコンストラクタの中でやればよいだけの話だから… 実際それは実行されているから,このコードは不要だ.⇒TRASHCAN::CleanSansyoにもNAMEBOXを参照しているコードが入っていたが,まったく不用なのでカットした.これでTRASHCANは完全にnoduleネイティブなクラスとして確立された.UNDOシステムを切り離すのも難しくないと思われるのでやってみよう.⇒実装した.

▲リサイクルシステム,UNDOシステム,参照リスト管理を止めたシステムを起動→カードを3枚一括削除してFAMILYTREE::GetCardBaseで(PHASE < DRAWSTAGE)エラーが出た.フェーズはINITIALIZEDになっている.UNDOの中で実行していた系統並び替えが実行されていないためだろう.カード削除処理中にMARGBOX::getGenerationが実行され,(coordinate() == ABSOLUTE)で停止するという事象も起きる.

UndoShadowCount, UndoShadowSizeという2つの変数を追加して解決

MemoryBlockCountとMemoryBlockSizeが実際と合わないという不具合がぶり返してしまった.カードを3点削除→UNDOSYSTEM:CommandEndで系統並び替えを実行する直前だ.totalcount 10165に対し,MemoryBlockCountは10218になっている.つまり,デクリメントされていない.カードを1点削除するだけで再現できる.カウントで24の差異が出る.CANとNringの合計とオブジェクト数=生成オブジェクト数ーリサイクル個数は一致している.カードを削除した場合,UNDOオブジェクトが生成されるので総数では増加しなくてはならない.従って,間違っているのはtotalcountの方である可能性がある.⇒UNDONODEは24個生成されてNringに入っている.nodecountが更新されていないのではないか?⇒いや,nodecountは最新だ.

ゴミ箱のカウントが+91, Nringが-24でMemoryBlockCountは+91.つまり,Nringからゴミ箱に移動した分がそっくり消えている.MemoryBlockCountはヒープから実メモリを取得したときにしかインクリメントされていないので,この差異がどこから発生しているのかよく分からない.⇒FAMILYTREE::DeleteCardDataの実行で91件の新規メモリ取得が発生している.同じ件数だけゴミ箱が増えているのだが,問題はNringの純減-24がどこに行ってしまったか?という点だ.

少なくともメモリからパージされていないのだから,ゴミ箱以外には往き場所はないのだが… Nringからパージされているノードは208件ある.これらはすべてゴミ箱に移動しているものと推定されるが,ゴミ箱はそこまで増加していない.ますます訳が分からなくなってきた… どうもこの統計はかなり怪しい.MemoryBlockCountをデクリメントしている関数が2つある.block::delmemと_delmemだ._delmemからdelmemを呼び出すようにして,1箇所で管理するようにしておこう.これでメモリ取得・解放の窓口はgetmem, delmemに1本化できた.

getmem/delmemはシステムで使用する全メモリを管理しているので,MemoryBlockCountとMemoryBlockSizeは全メモリに対応している.サイズ不定のフリーメモリは通常freeblockオブジェクトとして生成されるが,中には完全なフリーメモリとして扱われるものもある.UNDOのShadowイメージはその典型だ.というか,おそらくそれ以外では使われていないのではないかと思う.バッファなどはどうか?その辺りもそうなっているかもしれない… ⇒少なくともQUICKDBで使われているバッファはfreeblockで取得したものだ.

getmemで直接フリーメモリを取得している例として,COUPLING:SaveFamilyTreeで使っている人名/結婚リンクバックアップ領域というのがあるが,その関数内で解放されている.それ以外ではUNDOのShadowしかない.getmemではどこから呼び出されているか判別できないので,呼び出し元のUNDOSYSTEM::SetUndoListで管理するしかないだろう.暫定的にUndoShadowCount, UndoShadowSizeという2つの変数を作ってみた.⇒フリーメモリの取得はこれでよいとして,削除にどう対応するか?⇒UNDONODE::Disposeでfreeblock::delmemしている.ここで対処すればよい.⇒準備は整った.数字を見てみよう.

完全に一致した!これで問題は解決した.グローバルメモリの収支はこれで完全に押さえることができたと思う.UNDOの動作を確認してみよう.⇒ReferenceControlで参照カウントと参照リストの不一致が発生した.昨日はゴミ箱なしでこの不良が出ていたが,ゴミ箱ありでも同じ結果だ.UNDONODE::UndoRestoreで「オブジェクトの参照を一旦すべてクリアする」ということをやっている.ここでは対象ノードのすべてのスロットを検査して参照解除を実施しているが,スロット7,  8, 39と進んだところでエラーになっている.これはかなりおかしい.

スロット7, 8で問題なく処理できたのに39で突然不一致になるというのは考え難い※.障害ノードは#977 baselist 基本世代枠@33のCARDLINKでスロット39というのは,結婚ページ0に当たる.このカードは親ページを2, 結婚ページ1を持っている.ここで不一致が生じるというのはノーマルな動作なのではないかと思う.⇒いずれにしても参照リストと参照カウントの不一致は避けられない,UNDOに対応した処理が入っていないのだから… ⇒この問題は保留としておこう.

※これはちょっと勘違いしている.参照リストというのは被参照ノードが持っているリストで,参照元のスロットを連続検査しているが,対象の参照リストはそれぞれ独立のものだ.

▲参照リスト管理をUNDOと両立させる

ゴミ箱ありでカードを3枚削除→アプリ終了してfreeblock::delmemでエラーが発生した.引数のダブルポインタが空を指している.フロート状態のノードを削除しようとしているためだ.DoublePtrは親リンクが空の場合はNULLSPOTを返している.これはNULLが格納された場所を指している.freeblock::delmemでこのエラーが出るということは,この関数ではフロート状態のオブジェクトをdeleteできないということを意味する.これは仕様的にはやや問題があるが,実際問題として,ゴミ箱に入っているノードが親なしという状態自体がイレギュラーだ.

なぜこんなことが起こるのかについては,別途調べる必要がある.暫定的にNULLSPOTにこのノードのアドレスを格納して返すようにした.このアドレスはメモリをパージした後直ちにNULLに戻されるので,ここだけの話であれば,これでも悪くはないのだが…

CAN::ThrowCanを実行中,QNが負というエラーが発生した.printnamaeでバッファオーバーランが発生している.⇒修正した.

上記でfreeblock::_delmemの仕様を変更して,delmemを呼び出すように修正しているが,やはりこれでは通らない.これらの2つは用途が微妙に異なるので,それぞれ独立に併存させるしかない.

  1. freeblock::_delmemとdelmemはともにメモリ上の要素ブロックを解放するための関数で,事後にMemoryBlockCountとMemoryBlockSizeを更新する.また,これらの関数では要素のアドレスが格納されたポインタ(スロットないしベタ参照)を(可能な場合には)空にする.
  2. freeblock::delmemは汎用的なメモリ削除関数でfreeblockを含むnoduleクラスオブジェクトの他,完全なフリーメモリブロックも扱うことができるが,メモリブロックを参照するリンクに空を書き込む必要があるため,引数ではvoid**を取っている.
  3. この仕様では,フリーメモリを関数内部で(ローカル変数を使って)getmemして廃棄することはできないということになる.この制限はかなりきついかもしれない.厳格過ぎるかもしれないが,安全ではある.フリーメモリを解放するためには,それが「どこに置いてあるか?」を知らなくてはならない.
  4. freeblock::_delmemはnoduleクラスオブジェクトをメモリからパージするために用いる.noduleは通常メモリからパージされる時点ではフロート状態で所在不明(接続先不明)となっているため,delmemにダブルポインタの形式で渡すことができない.

もし,これらを統一して単一の関数でMemoryBlockCountとMemoryBlockSizeを管理したいというのであれば,delmemと_delmemの両側から呼び出されるフラットな関数を用意するしかない.実装は簡単なのでやってみよう.getmemに対応するフラットな関数を改めてdelmemとし,現行のdelmemはdelmem_とリネームする.delmemは外部から使えないようにprivateとした.

▲MemoryBlockCountの不整合がぶり返してしまった.カードを1点削除→アプリ終了して,totalcount 1 MemoryBlockCount 10, TotalSize 1180 MemoryBlockSize 27576となった.要素数で9個,サイズで26396のオブジェクトが紛失している.ThrowCan 前 diff=0 10302 = waste 10301 + Nodecount 1 =10302となっているので,NringにはCANしか残っていない.残りはすべてゴミ箱の中とUNDOのShadowだけだ.NringTotal 1180+CanTotal 7023434+UndoShadowSize 7024614は,MemoryBlockSize 7024614と一致している.

ThrowCan 後ではUndoShadowSize 0となっているので,UNDOのShadowは完全にパージされている.GlobalFreePtrを実行しているのはfreeblock::delmemしかないし,この関数の中でしかMemoryBlockCountとMemoryBlockSizeは更新されていないのだから,間違いようがないとしか思えないのだが…

単にファイルを開いて閉じるではこのエラーは発生しないので,カード削除とそれに伴うUNDO処理にからんだ問題であることは確かだが… ⇒いや,少なくともUNDOとは関わりがないのではないか?UNDOはCOUPLING::EraseFamilyTreeの中ですでにパージされている.上で,

NringTotal:1180+CanTotal:7023434+UndoShadowSize=7024614

としているが,間違いだ.プリント文の誤記で,実際は

NringTotal:1180+CanTotal:7023434+UndoShadowSize=0 = 7024614

でUndoShadowSize=0となっている.つまり,UNDOのShadowはすでにパージ完了している.何度数え直してもゴミ箱の中は個数で10301,サイズで7023434だ.結局,ThrowCanのどこかで漏れが発生していると考えるしかない.⇒成仏しきれない亡者を9名発見した.CARDLINKが7つ,MARGLINKが2つ.deleteでメモリからパージされるためにはNODULEのoperator deleteが起動されなくてはならないが,何かの理由でそうならなかったのだろう.⇒NODULE::operator deleteには来ているが,LIFEが空になっていないためパージを免れている.

大体の様子はつかめた.これらのノードはアプリ終了時,EraseFamilyTreeでLINKTABLE::ClearTableによって削除されている.ただし,このときはShadow付きであるためゴミ箱には直行せず,Nringの中で保留状態になっている.この後のresetUndoChainでゴミ箱に移動しているようだ.CallSetCouplingPtrがアプリから呼び出されるときには,すでにNringにはコア骨格木しか残っていない.

resetUndoChainには明示的に関係オブジェクトをゴミ箱に移すようなコードは存在しない.単にコマンドチェーンのUNDOCHAINを1個づつdeleteしているだけだ.とすれば,UNDONODE:Disposeしかない.⇒確かにそのようだ.ここではUNDONODEが参照している実ノードをDumpWasteでゴミ箱に投入している.しかも,その前にsetNring(address, NRING_DELETE)を実行しているのだから,息の根が止まってもよさそうなものだが… ⇒setNringではShadowが残っているため,Shadow->DELETED = DEADとしている.もし,Shadowが付いていなければLIFEに空がセットされているのだが…

UNDOチェーンの長さは有限なので満杯になると後ろからチェーンを切断するような動作になっているはずだ.従って,UNDONODEが削除されたことで参照している実ノードが削除されたことには必ずしもならないというのは正しいと思われるが,現行ではゴミ箱に入るときはすでに死亡しているという前提なので矛盾が生じる.どう解決すればよいか?

アプリ終了時のMemoryBlockCountとMemoryBlockSizeの値が整合しない

アプリ終了時のMemoryBlockCountとMemoryBlockSizeの値が実際のカウントと整合しない.⇒ゴミ箱の廃棄をCouplingの削除の後に実行するようにした.ゴミ箱はCouplingの生成に先立って生成されているので,この方が筋が通っている.また,ゴミ箱とNringが存続していないと,メモリの使用状況が正確に把握できない.この変更を行ったことで,TRASHCAN:CheckWasteCountでも不整合が検出されるようになった.オブジェクトのカウントで32の差異が発生している.

この差異がCouplingの削除によって発生していることは明らかだ.つまり,この間に32個のオブジェクトが紛失していることになる.おそらく,これらはフロート化されたまま放置されているものと思われるが,そうではないかもしれない.フェーズをENDOFAPPLICATIONに切り替えるのをゴミ箱廃棄直前まで延期してこのエラーは解消した.

★ThrowCan 前 totalblock=0 totalsize=0 MemoryBlockCount=10037 MemoryBlockSize=8098118 TotalBlockCount=10037 TotalBlockSize=8098118

★ThrowCan 後 totalblock=0 totalsize=0 MemoryBlockCount=1 MemoryBlockSize=1180 TotalBlockCount=10037 TotalBlockSize=8098118

ThrowCan後に残っているMBCount=1, MBSize=1180とはゴミ箱のことだが,なぜこれが残ってしまうのかはよく分からない.⇒NODULE:operator deleteが呼び出されていない.強制的にNODULE::operator deleteを実行してやれば確かに消える.なぜCANだけがこのような動作になるのか?その理由が分からない.ゴミ箱TRASHCANは

class TRASHCAN : public ARRAY<MAXTRASHCAN>
class ARRAY : public nodule
class nodule : public NODULE {
static TRASHCAN *CAN; // ゴミ箱(廃棄オブジェクト分別収集)

のように定義され,メモリ上では

TRASHCAN *nodule::CAN = NULL; // ゴミ箱(廃棄オブジェクト…

のように配置されている.通常のオブジェクトは一度ゴミ箱に入ってから最終処分される.ゴミ箱自身はゴミ箱に入っていないということが影響しているのだろうか?ゴミ箱はNringに入っているか?⇒入っている.ゴミ箱廃棄直前ではNringにはこれしか入っていない.TRASHCAN:ThrowCanとdelete CANは別々に実行されている.

delete CAN

を実行したとき,NODULE::operator deleteが実行されないという点を除けば,すべての動作が整序している.CANに特異な点があるとすれば,noduleクラスの静的メンバーになっているという点だが,静的とは言え,ただのポインタなので中身のオブジェクトの生成・削除とは関わりがない…何かコンパイラが誤動作しているか,誤認しているのではないかという気がするのだが,おそらく,CANをNODULEの静的メンバーにしてしまえばこういうことは起きないのではないかという気もする.試してみよう.一度バックアップを取ってから…

その前にこのシステムがゴミ箱がなくても動作することを確認しておいた方がよいのではないか?USERECYCLESYSTEMというオプション(SPECIFICATION)はあるが… ⇒ゴミ箱がなくても動作には支障はないが,MemoryBlockCount=118005 MemoryBlockSize=25116470が丸残りになってしまう.現物はdeleteでメモリからは解放されているはずなので,残っているのはただの数字と思われるが…

この数字はnewで生成されたすべてのオブジェクトを含んでいる.つまり,カウントがまったくデクリメントされていない.ということはおそらく,NODULE:operator deleteが一度も実行されていないことを意味すると思われるのだが… どう考えればよいのだろう?

どうもこれは思ったより難しい問題であったような気がする.「ゴミ箱を生成しない」システムでアプリ終了しようとしたら,ReleaseReflistでエラーが発生した.NODULE::operator []が動作しないというエラーだ.対象ノードはすでに削除されているので,NODULEの仮想関数テーブルが潰れている.参照リスト管理ではこれまでオブジェクトの生死に関わりなく参照管理を行うという方針で進めてきているが,それもこれもゴミ箱のようなシステムがあったお陰と言ってもよい.つまり,nodule::operator deleteで寸止めしているため,仮想関数テーブルが辛うじて使える状態になっていたのだろう.

オブジェクトの生死を問わないというのは参照リスト管理に限った話ではない.UNDOもそれがなければ成立しない.削除されたオブジェクトを復活させようという話なのだから… 従って,ZTではNODULE:operator deleteが作動しないというのがノーマルであり,作動しているとすればそれは例外であると考えなくてはならない.NODULE:operator deleteが作動する条件というのはあまりはっきりしないが,おそらく,「一度deleteされ,もう一度newによって再生されたオブジェクト」なのではないか?⇒これは確認してみなければ分からない.

いずれにしても,nodule::operator deleteの中からNODULE::operator deleteを実行することの可否は,少なくともこのシステムでは死活問題だ.死者の延命・復活という手段がなければZTシステムは成立しない.ただし,参照管理リスト上にすでに死亡したオブジェクトが入っているというのは少しおかしいような気がする.なぜこんな状態になっているのかということをまず,先に調べた方がよいと思う.そのノードはどこで死んだのか?そのノードを参照するリンクが残っているのはなぜか?

障害が起きているノードは#74のMARGBOXだ.このノードはEraseTreeViewの中で削除されている.CleanSlotは実行されているが,参照管理に使っている特殊スロットは対象外で,参照リストには12個も参照が残っている.しかし,ReleaseReflistでMARGBOXの参照リストを削除する段ではすでにリスト要素はゼロになっている.つまり,問題は死亡したノードに参照リストが残っているという点だけだ.しかし,これは仕様であり,譲ることはできない.

従って,結論的には「nodule::operator deleteの中からNODULE:operator deleteを実行するという解決策はない」とするしかない.NODULE::operator deleteでやっていることは以下の一行だけだ.

if (!nptr->Shadow && !nptr->LIFE && (INDELETEPHASE || !nodule::CAN)) freeblock::_delmem(nptr);

これをnodule::operator deleteで実行すればよいというだけの話なのではないか?⇒いや,結構難しい.ThrowCanでゴミ箱のオブジェクトをdeleteしようとしてもできない.フローでは_delmemをパスしているので,メモリ上には残っているし,仮想関数テーブルも活きているようだが delete wasteが実行できない.おそらく,nodule::operator deleteを再実行することが禁止されているのだろう.つまり,nodule:operator deleteとNODULE::operator deleteの2段構成はどうしても不可欠ということのようだ.

「死亡したノードに参照リストが残っているという点」という点をもう少し考えてみよう.本来死亡したノードへの参照はデストラクタの中ですべてクリアされるというのが本筋であり,参照カウントがゼロになったノードでは参照リストを削除することができるとすれば,通常なら~noduleで参照リストを解放できるはずだ.⇒うまくいった!~noduleで参照カウントが残っているオブジェクトは存在しない.従ってすべての参照リストはその時点で削除される.⇒もう一度,「ゴミ箱なしの動作を確認@20201214」を試してみよう.

今回はnodule::operator deleteの中から直接NODULE::operator deleteを呼び出すのではなく,処理の対象を限定して,Shadowを持たないノードのうち,ゴミ箱に入らないものでかつLIFEが空となっているものだけを_delmemするようにして動作するようになった.ただし,一つだけ問題がある.ThrowCanを実行後にはMemoryBlockCount=0 MemoryBlockSize=0のようにきれいにクリアされているが,resetNringの手前でMemoryBlockCount=-1 MemoryBlockSize=-156になる.⇒理由は明らかだ.その前にrootnodeを削除しているためだ.

rootnodeにはNODULEのメンバーのnodule nullpointが入っている.このオブジェクトはnodule クラスの内部オブジェクトだが,静的メンバーでnewで生成されたものではないので,_delmemする必要はない.どこでそれを判断すればよいか?MemoryBlockCountは_delmemで更新しているので,ここでゼロ復帰でよいのではないか?このオブジェクトがnullpointであるということは,snum=0であることで判定できる.また,ntype=NOTAUTODELETEであることからも識別できる.⇒OKだ.かなりクリーンなイメージに近づいてきた.

UNDOシステムはゴミ箱がなくても動作しなくてはならない.確認しておこう.⇒ReferenceControlで参照カウントと参照リストの不一致が発生した.ゴミ箱を復旧して,MemoryBlockCountとMemoryBlockSizeが実際と合わないという事象が復活してしまった.ファイルを開いて終了では発生しないので,これまで見過ごされていた可能性はある.

アプリから完全独立な抽象UNDO機能系

UNDOシステムの理想はアプリケーションから完全に独立な抽象機能系だ.UNDOはアプリが何をやっているのかを知らなくても対象オブジェクトの形式的な解析だけで保全する範囲・手順を決定できるものとする.そのようなものが存在するとすれば,アプリケーションではUNDOをまったく意識せずに自由にアプリ固有のコードを書くことができるし,システムメンテナンスのコストも大幅に削減されるだろう.そんなことが可能だろうか?もちろん,原理的には可能だ.コマンド実行ごとに系全体を「バージョン」としてバックアップするだけで簡単・確実に実現できる.ただし,この方式はコスト的に見て現実的ではない.

たとえば,テキストエディタのようなものであれば,テキスト編集は①挿入と②削除の2つのプリミティブな操作に還元できると考えられるから,①の場合は,位置と挿入文字列,②の場合は位置と削除文字数を記録するだけで基本的なUNDO機能をサポートできるかもしれない.これは言ってみればミニマムなUNDO系の実現と言える.しかし,ZTの場合なら,たとえば「カード登録」という一つのコマンドで,複数の関係(親子,結婚,兄弟など)の変化が同時多発的に発生し,それらが相互にどう影響し合うのかもにわかには判別できない.

しかし,完全な参照管理が実現された超クリーンで透明なシステムであればひょっとしてそのようなことができるのではないかと期待することはできる.ZTのUNDO機能はエラーが発生したときにUNDOで戻ってそれを再現できる程度には強力だが,まだそこまでの抽象度には達していない.ともあれ,まずいま出ている目先の問題から片付けることにしよう.どうもこの不良はシステム的なもの(設計不良)というより,むしろバグ(論理不良)ではないかという気がしてきた.少なくとも見た限りではNring上に同一snum(システム通番)を持つ複数のオブジェクトが存在しているように見える.まず,この点を確認してみよう.

誤読していた.Nring上には重複は存在しない.重複していたのはUNDONODEからの参照先だ.ただし,UNDONODEは複製ノードと一対一に対応していたはずだから,やはり重複があるというのは誤りなのではないか?⇒複製ノードが同一snumを持つ場合は当然あり得る.もちろん,実体が同じなら重複していることになるが… 問題なさそうだ.

「Nringとゴミ箱にダブル登録」という問題を考えなくてはならないが,その前にカード削除で新たにCARDLINKとMARGLINKが生成されるという動作を調べてみよう.⇒どうもまだ勘違いしているようだ.CARDLINKとMARGLINKがNringに残る問題と「Nringとゴミ箱にダブル登録」問題は完全に一つの問題だ.わかり易くするためにはダブル登録を止めるしかない.ゴミ箱に入っているオブジェクトはリサイクル可能というのが本来の趣旨なのだから,Shadow付きオブジェクトはリサイクル不可としてNringに置くべきなのではないか?⇒そのように決定.

nodule::DELETEDの値を{-1, 0, 1}ではなく,明示的に{PROLONG, EXISTING, DEAD}で示すようにしておこう.⇒nodule::operator deleteでShadow付きオブジェクトはゴミ箱に移動しないように修正して,CheckWasteCountでカウント不一致になった.⇒これまではNringのカウントとしてアクティブノードだけを計上していたが,延命ノードの重複が解消したので,全ノードをカウントする必要がある.⇒これで中間の集計はすべて一致するようになったが,アプリ終了の出口関数で不一致になった.ゴミ箱の中身が25点少なくなっている.

この差分はReleaseRefListの実行によって起きている.いや,違う.EraseFamilyTreeの中だ.UNDOSYSTEM::resetUndoChainだ.⇒UNDOSYSTEM::CheckUndoChainでエラーが出た.親ノード不在(!undo->address->getpnode())というエラーだが,これは「Nringとゴミ箱の重複登録を禁止@20201213」の結果で問題ない.ゴミ箱に入っているということは親を持っているということになるが,Nringに入っているPROLONGノードは親を持たないフロート状態になっている.

▲初回のEraseFamilyTreeのとき,すでにMemoryBlockCount=32 MemoryBlockSize= 143564がリザーブされている.常駐させるか,ないし,取得を遅延させるか.

▲UNDONODE::DisposeでShadowオブジェクトを直接delmem_でメモリからパージしている.実際にはこの前段でfreeblockはすべて解放されているので,実際の効果はないのだが,あまりよいマナーではない.ここはfreeblockのアドレスを取得して delete するべきだ.

UNDONODE::DisposeでPROLONGオブジェクトの始末を付けるようにした.⇒エラーはすべて解消した.手順としては,フリーブロックの解放はUNDOSYSTEMのリセットより後にするべきだと思う.順序を入れ替えてみよう.⇒UNDONODE::Disposeでshadowを保持するfreeblockをdeleteしようとしてエラーになった._getblock_でfreeblockの逆引きができない.shadowはfreeblock::getmemで取得したアドレスなので,_getblock_でfreeblockのアドレスを取れるはずでは…

いや,違う.勘違いしていた.shadowは完全なフリーメモリだ.つまり,getmemで取得し,delmem_でリリースするという類のものだ.delmem_は下位関数としてdelmemを呼び出しているが,バランスが悪い.getmemの対向としてはdelmemの方がよい.名前を取り替えておこう.⇒現行ではアプリ終了時にゴミ箱内の廃棄物を処分している.freeblockもリサイクル可能なのだから,中間で廃棄する必要はないのではないか?さらに言えば,最終的にはfreeblockもゴミ箱に落ちるのだから,アプリ終了時にはゴミ箱の処分だけやればよいのではないか?

上記の通り,freeblock::ReleaseFreeBlockを止めてTRASHCANに任せたところ,freeblockが2つ残留した.#34と#36だ.基本的にfreeblockは作成者が責任をもって始末すべきものだ.⇒#34はBUNSFILE::openで取得しているファイル読み込みのためのバッファだ.この関数が呼ばれるたびに,前回取得したメモリを解放しているので,関数の出口で解放してよいのではないか?closeという関数はある.BUNSFILE::closeには,「オブジェクト存続中はバッファbunsbuffを維持する」という説明が付いている.~BUNSFILEで解放している.

QUICKDBはKAKEIZUの埋め込みオブジェクトなのでデストラクタは呼び出されない.KAKEIZUはQUICKDBを2つ持っているが,これらをnewで生成してやれば,明示的にdeleteすることもできるのだが… ファイルをクローズしてもバッファを使うというのは考え難いので,Closeで廃棄しておこう.closeはCloseFamilyBaseのタイミングで掛かってくるのでまったく問題ない.一度ここでバックアップを取っておこう.

freeblockでMemoryBlockCountとMemoryBlockSizeが更新されていない.totalblock, totalsizeは更新されている.⇒いや,totalsizeはゼロのまま変化していない.⇒コードを追加した.totalblockはfreeblockのカウント, totalsizeはfreeblockのヘッダ部を除いたフリーメモリの正味サイズの合計だ.Couplingを削除してMemoryBlockCount=33 MemoryBlockSize=143756まで落ちる.メモリリークの可能性はあるが,CrtDumpMemoryLeaksのダンプはもっとずっと小さな数字だ.

★DRAWSTAGE totalblock=42 totalsize=169966 MemoryBlockCount=10033 MemoryBlockSize=6975606 TotalBlockCount=10033 TotalBlockSize=6975606

★INITIALSTATE totalblock=0 totalsize=1291806 MemoryBlockCount=10037 MemoryBlockSize=8098118 TotalBlockCount=10037 TotalBlockSize=8098118

★delete Coupling totalblock=0 totalsize=1291806 MemoryBlockCount=32 MemoryBlockSize=143564 TotalBlockCount=10037 TotalBlockSize=8098118

アプリ終了時でtotalblock 0 totalsize 1291806 MemoryBlockCount 32 MemoryBlockSize 143564 のメモリが紛失していることになる.freeblockではCloseFamilyBase→EraseFamilyTreeの入口で不一致が発生している.KAKEIZU::closeFamilyBaseの中で不一致になる.qfile.closeで起きるようだ.バッファのサイズが変化しているのではないだろうか?⇒freeblock::delmptrでtotalsizeを更新していなかった.freeblockの数字は合うようになったが,MemoryBlockCountは33も残っている.サイズで143756の残がある.

わかり易くするためにfreeblock::delmptrとfreeblock::delmem_を廃止して,_delmem_とdelmemに統合した._delmem_に回ってくる分に関してはMemoryBlockCount,MemoryBlockSizeの更新が入っていなかったので,その論理を追加したところ,MemoryBlockCount=-9 MemoryBlockSize=143588という不整合が出た.MemoryBlockSizeの減量をオブジェクトsizeから取得するようにして,MemoryBlockCount -9 MemoryBlockSize -33770という結果になった.

これはおそらくdeleteで一度ゴミ箱に入ったものが再度deleteされているためと考えられる.freeblock::_delmemやfreeblock::delmemで削除される場合は実際にメモリからパージされているので問題ない.NODULE::operator deleteではfreeblock::_delmem(nptr)でメモリ解放しているので,_delmem_で引くというのはやはり間違っていると思う.つまり,消し忘れが33件残っているということではないだろうか?

清掃工場で生ゴミの処理がストップしている

カードを削除後,アプリ終了してゴミ箱の中身を処分する段階で,「生ゴミだ!」というエラーが出ている.「生ゴミ」というのは,UNDOでバックアップしたオブジェクト・イメージが未処理の状態で残っていることを意味する.生ゴミはCOUPLING::EraseFamilyTreeでReleaseShadowによって処理されているはずだが,どうもそれが機能していないようだ.昨日のログでは「始業時バックアップではこのようなエラーは出ていない」としているが,これは事実誤認で,その前日2020/12/10の始業時バックアップでもすでにこの現象は出ている.

2020/12/09には「ここまでの修正をすべて現状でフィックスしておく」として仕掛りになっているすべてのOPTIONSを一掃してしまっているので,これ以前のバージョンに戻ることは難しい※.UNDOの論理は結構複雑で動作の解析も難しいが,「後戻りしない」と決めたのだから,このまま進むしかない.※⇒【2020/12/08の始業時バックアップでも同じ現象が出る.】⇒nodule::ReleaseShadowで

if (nod->Shadow && !nod->Shadowed()) {

の論理を逆転して,

if (nod->Shadow && nod->Shadowed()) {

のように変えたら,エラーは解消した.どうもShadowed()という関数の意味を逆に解釈しているのではないかという気がする.ただし,この修正を行うことでTRASHCAN::ThrowCanはエラーなしに終わるが,その後の delete Coupling でUNDOSYSTEMを解体するところでsetNringのエラーが発生する.どちらにしてもこのエラーはUNDOSYSTEMを解析しないと対処できないので,少し調べてみよう.

image

この図面は縦長なので,タブレットを90度回転させて撮ってやろうと思ったのだが,タブレットにはゼルコバの木を一度もインストールしたことがない.最新リリース版は一度VAIOにインストールしているので,簡単に終わるはずだったのにエラーになってしまった.そう言えばVAIOのときにも何かあったなと思い出してログを引っ張りだ出そうとしたが,あいにく現状ではOpen Live Writerの全文検索ができない.

しかし,それほど昔の話ではないのでサイトで検索することにした.ChromeのウェブストアにGoogle検索を使ったプラグインがあったので使ってみた.サイト内検索ができるというのはかなり有り難い.結局,VS2017に対応する再配布パッケージが必要ということがわかったので,Visual C++ 2017 Redistributable (x86)をマイクロソフトサイトからダウンロードしてインストール,これでZTもインストールできた.

UNDOでは何かコマンドを実行しようとするときには,事前に更新の予測されるオブジェクトのイメージのバックアップを取って保全する.ただし,事前と事後という区分けはややあいまいで,あるコマンドの事後は次のコマンドの事前でもあるから,コマンド実行後にも変化があればそのイメージが保存される.事前に予測して保全するというのはややムダのある動作※だから本来ならコマンド処理の中でデータが更新される直前にその部分だけをバックアップすればよいのだが,そうなるとコマンド処理とUNDO処理の境界が消えてとんでもなくややこしいものになってしまう虞があるため,現行のような仕様になっている.

※多分現行では事後に保全データをチェックして変化なしの場合にはそのデータを破棄していたはずだ.

UNDOSYSUTEMは一つのコマンドに対して一つのUNDOCHAINノードが対応したコマンドのチェーンを持っている.一つのUNDOCHAINはそのコマンドで保全された個別オブジェクトの複製イメージを保持するUNDONODEのチェーンを持っている.ここで問題なのはこのオブジェクトのイメージはどういうデータ構造になっているのか?という点だ.複製ノードをすべてNODULEオブジェクトとして生成することは可能だが,コストが掛かり過ぎる.それではどうしているのか?

オブジェクトの複製はfreeblockで必要サイズのメモリを確保し,そこにオブジェクトのイメージを転記している.freeblockはEraseFamilyTreeの中でまとめてパージされている.また,複製イメージへのリンクはベタ参照なので参照管理とは無関係に削除できる.UNDONODEはチェーンを構成するためのzenpoUndoとkohoUndoという2つのリンクスロットの他,実ノードの物理アドレスを指すaddressと複製ノードへのべた参照を格納するshadowを持っている.

オブジェクトは生成から消滅までの間に物理アドレスが変化することはないので,UNDONODEから実ノードへのaddress参照は状態に関わらずつねに有効だ.UNDONODEと複製ノードは1対1に対応しているが,一つのオブジェクトが複数の複製ノードを持つ場合があり得る.つまり,実ノードと複製ノードの関係は1対多であるため,NODULEのShadowという特殊スロットでチェーンを構成し,これを管理している.

オブジェクトは複製ノードを持っている間,つまり,Shadowが空でない間は削除されないということになっている.従って,削除されてもゴミ箱には入らず,フロート状態でNringに繋がっていると考えられる.(実際にそうなっているかどうかは分からない.前にゴミ箱とNringに二重登録されているという話があった)さて,これで大体のイメージは掴めたので,実際にどういう動作になっているのかを確かめてみよう.

まず,削除などの操作を一切行わず,ファイルを開いて閉じるだけの動作を確認しておこう.⇒アプリ終了時,CallSetCouplingPtrの入口ではアクティブオブジェクト 32(19クラス)というコア骨格木と呼んでいる状態になっている.ゴミ箱には10003個(20クラス)の廃棄オブジェクトが入っている.カードを3枚削除して終了した場合を見てみよう.⇒Nringには,281個(25クラス)のオブジェクトが入っている.オブジェクト数で249,クラス数で6増加している.内訳で見ると,

  • nodule 114
  • UNDOCHAIN 2
  • UNDONODE 53
  • MARGLINK 10
  • NODEREFLIST 55
  • CARDLINK 15

となっている.これらはすべてUNDOに関わりのあるノードと思われる.UNDOチェーンをパージするコマンドがあったはずなので探してみよう.UNDOSYSTEM::resetUndoChainという関数がある.これを実行したらどうなるか?⇒完全にコア骨格木を復元できた.ただし,その分ゴミ箱の中身は増えて10410個(22クラス)に変化している.何もしないときのオブジェクト総数は10032,3カード削除したときには407個増加して10410.仕様的にはこれで動作は完全と言えるところだが,なぜ生ゴミが出るのか,ないし,なぜReleaseShadowは機能していないのかというナゾはまだ解き明かされていないので調べておこう.

EraseFamilyTreeでfreeblockをパージする前の状態を見ると,Nringの中身はコア骨格木の状態,freeblockはゼロ,ゴミ箱の中身は空だ.しかし,これでは辻褄が合わない.1万個近いオブジェクトがどこかに消えてしまっている.そんなバカな… ⇒アプリ開始から終了までの間にEraseFamilyTreeは複数回呼び出されている.初回がまったくの空であるのは当然だ.アプリ終了時,freeblock解放の直前では,Nringは1396個(21クラス),ゴミ箱8639個(19クラス)だ.Nring1396個のうち,1361はNODEREFLISTで,3個がfreeblock,残り32個がスケルトンだ.この3個のfreeblockがUNDOで生成された複製ノードと思われる.

いや,違う.上の数字は「何もしていない」ときの数字だ.このfreeblock3個が何の用途で作られているのかは分からないが,UNDOの複製ノードでないことは確かだ.実際,この3個を除けばトータル10032となり何もしないときのオブジェクト総数と一致する.freeblockはReleaseFreeBlockでメモリから完全にパージされてしまうので,ゴミ箱にも残らない※.カードを3つ削除した結果を見てみよう.Nringには1648個(26クラス)のオブジェクトが残っている.ゴミ箱には8819個(19クラス)入っている.※⇒【間違っている.ENDOFAPPLICATIONフェーズまではメモリからはパージされず,ゴミ箱に残っている.】

Nring上のfreeblockの個数は3で変化していない.UNDOCHAIN x 2, UNDONODE x 53がUNDOチェーンのために追加されている.目に付くのはCARDLINK x 15,MARGLINK x 10 が入っているという点だ.ゴミ箱にはCARDLINK x 189,MARGLINK x 88 が入っているが,これらは現物の人名リンクと結婚リンクと考えられるので,Nringに入っているのはすべてUNDOのために新たに生成されたもののように思われる.複製はfreeblockのメモリ上にコピーされたものと思っていたが,どうも現物のコピーを使っているようだ.

実行されたコマンドは「カード削除」の1件だけだが,UNDOCHAINは2個生成されている.これは「カード削除」の前と後が保全されているためで,UNDOCHAINは存在するとすればつねに2個以上になる.CARDLINK 15, MARGLINK 10 が複製ノードだとしても,UNDONODE 53 をカバーすることはできない.プリミティブなオブジェクトとして無名のnoduleが114個もあるので,これらの中にそれらが含まれている可能性はあるが,どうやってそれを分離することができるだろう?

ゴミ箱にはfreeblockが39個入っている.これらがすべてUNDOの複製ノードであるとすると,CARDLINK 15, MARGLINK 10と合わせて64個となり,今度はUNDONODE 53 を超過してしまう.どうやったらこの帳尻を合わせることができるのだろう?ゴミ箱とNringのダブリは25でこの数字はDELETEDがNRING_INVALIDATEであるノードの個数と同じだ.この数はNring上のCARDLINKとMARGLINKの総数に等しい.つまり,これらはすべてNRING_INVALIDATEとされるようなノードということになる.整理してみよう.

  1. Nring上のノード総数 1648 
  2. ゴミ箱上のノード総数 8819
  3. Nringとゴミ箱の二重登録 25 ー非アクティブノード
  4. Nring上のCARDLINK 15 + MARGLINK 10 = 25 =(3)
  5. Nringとゴミ箱の合計(ダブリを除く)10442
  6. アプリ終了時のノード総数 10442

これで一応帳尻は合った.問題はUNDONODEの53という数字の内訳だ.⇒内訳もわかった.CARDLINK x 30, MARGLINK x 18, PARTIALNAME x 3, longtable x 2だ.