Commit c75b1c6f authored by Kirill Smelkov's avatar Kirill Smelkov

X wcfs/xbtree: Start killing holeIdx

parent 3c2a1e25
...@@ -89,6 +89,7 @@ type ΔValue struct { ...@@ -89,6 +89,7 @@ type ΔValue struct {
} }
/* XXX kill
// treeSetKey represents ordered set of keys. // treeSetKey represents ordered set of keys.
// it can be point-queried and range-accessed. // it can be point-queried and range-accessed.
// TODO -> btree // TODO -> btree
...@@ -107,6 +108,7 @@ func (hi treeSetKey) GetInRange(lo, hi_ Key) SetKey { ...@@ -107,6 +108,7 @@ func (hi treeSetKey) GetInRange(lo, hi_ Key) SetKey {
} }
return ret return ret
} }
*/
// δZConnectTracked computes connected closure of δZ/T. // δZConnectTracked computes connected closure of δZ/T.
// //
...@@ -341,10 +343,7 @@ func (rs rangeSplit) String() string { ...@@ -341,10 +343,7 @@ func (rs rangeSplit) String() string {
// δtops is set of top nodes for changed subtrees. // δtops is set of top nodes for changed subtrees.
// δZTC is connected(δZ/T) - connected closure for subset of δZ(old..new) that // δZTC is connected(δZ/T) - connected closure for subset of δZ(old..new) that
// touches tracked nodes of T. // touches tracked nodes of T.
// func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, trackSet PPTreeSubSet, zconnOld, zconnNew *zodb.Connection) (δT map[Key]ΔValue, δtrack *ΔPPTreeSubSet, err error) {
// XXX holeIdx is updated XXX -> return similarly to δtrack
// XXX ^^^ -> but better kill holeIdx and do everything only via trackSet
func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, trackSet PPTreeSubSet, holeIdx treeSetKey, zconnOld, zconnNew *zodb.Connection) (δT map[Key]ΔValue, δtrack *ΔPPTreeSubSet, err error) {
defer xerr.Contextf(&err, "treediff %s..%s %s", zconnOld.At(), zconnNew.At(), root) defer xerr.Contextf(&err, "treediff %s..%s %s", zconnOld.At(), zconnNew.At(), root)
tracef("\ntreediff %s δtops: %v δZTC: %v\n", root, δtops, δZTC) tracef("\ntreediff %s δtops: %v δZTC: %v\n", root, δtops, δZTC)
...@@ -361,7 +360,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t ...@@ -361,7 +360,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t
return nil, nil, err return nil, nil, err
} }
δtop, δtrackTop, err := diffX(ctx, a, b, δZTC, trackSet, holeIdx) δtop, δtrackTop, err := diffX(ctx, a, b, δZTC, trackSet)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
...@@ -380,6 +379,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t ...@@ -380,6 +379,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t
δtrackv = append(δtrackv, δtrackTop) δtrackv = append(δtrackv, δtrackTop)
} }
/* XXX kill
// adjust holeIdx // adjust holeIdx
for k, δv := range δT { for k, δv := range δT {
if δv.Old == VDEL { if δv.Old == VDEL {
...@@ -389,6 +389,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t ...@@ -389,6 +389,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t
holeIdx.Add(k) holeIdx.Add(k)
} }
} }
*/
// adjust trackSet by merge(δtrackTops) // adjust trackSet by merge(δtrackTops)
δtrack = &ΔPPTreeSubSet{Del: PPTreeSubSet{}, Add: PPTreeSubSet{}, δnchildNonLeafs: map[zodb.Oid]int{}} δtrack = &ΔPPTreeSubSet{Del: PPTreeSubSet{}, Add: PPTreeSubSet{}, δnchildNonLeafs: map[zodb.Oid]int{}}
...@@ -410,7 +411,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t ...@@ -410,7 +411,7 @@ func treediff(ctx context.Context, root zodb.Oid, δtops SetOid, δZTC SetOid, t
// //
// δtrack is trackSet δ that needs to be applied to trackSet to keep it // δtrack is trackSet δ that needs to be applied to trackSet to keep it
// consistent with b (= a + δ). // consistent with b (= a + δ).
func diffX(ctx context.Context, a, b Node, δZTC SetOid, trackSet PPTreeSubSet, holeIdx treeSetKey) (δ map[Key]ΔValue, δtrack *ΔPPTreeSubSet, err error) { func diffX(ctx context.Context, a, b Node, δZTC SetOid, trackSet PPTreeSubSet) (δ map[Key]ΔValue, δtrack *ΔPPTreeSubSet, err error) {
if a==nil && b==nil { if a==nil && b==nil {
panic("BUG: both a & b == nil") // XXX -> not a bug e.g. for `ø ø T` sequence? panic("BUG: both a & b == nil") // XXX -> not a bug e.g. for `ø ø T` sequence?
} }
...@@ -445,7 +446,7 @@ func diffX(ctx context.Context, a, b Node, δZTC SetOid, trackSet PPTreeSubSet, ...@@ -445,7 +446,7 @@ func diffX(ctx context.Context, a, b Node, δZTC SetOid, trackSet PPTreeSubSet,
} }
if isT { if isT {
return diffT(ctx, aT, bT, δZTC, trackSet, holeIdx) return diffT(ctx, aT, bT, δZTC, trackSet)
} else { } else {
var δtrack *ΔPPTreeSubSet var δtrack *ΔPPTreeSubSet
δ, err := diffB(ctx, aB, bB) δ, err := diffB(ctx, aB, bB)
...@@ -460,7 +461,8 @@ func diffX(ctx context.Context, a, b Node, δZTC SetOid, trackSet PPTreeSubSet, ...@@ -460,7 +461,8 @@ func diffX(ctx context.Context, a, b Node, δZTC SetOid, trackSet PPTreeSubSet,
// //
// a, b point to top of subtrees @old and @new revisions. // a, b point to top of subtrees @old and @new revisions.
// δZTC is connected set of objects covering δZT (objects changed in this tree in old..new). // δZTC is connected set of objects covering δZT (objects changed in this tree in old..new).
func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet, holeIdx treeSetKey) (δ map[Key]ΔValue, δtrack *ΔPPTreeSubSet, err error) { func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet) (δ map[Key]ΔValue, δtrack *ΔPPTreeSubSet, err error) {
// var holeIdx treeSetKey XXX kill
tracef(" diffT %s %s\n", xidOf(A), xidOf(B)) tracef(" diffT %s %s\n", xidOf(A), xidOf(B))
defer xerr.Contextf(&err, "diffT %s %s", xidOf(A), xidOf(B)) defer xerr.Contextf(&err, "diffT %s %s", xidOf(A), xidOf(B))
...@@ -518,21 +520,31 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet, ...@@ -518,21 +520,31 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet,
Bv := rangeSplit{btop} // nodes expanded from B Bv := rangeSplit{btop} // nodes expanded from B
// for phase 2: // for phase 2:
Akqueue := SetKey{} // queue for keys in A to be processed for δ- Akqueue := SetKey{} // queue for keys in A to be processed for δ- XXX -> KeyRangeSet
Bkqueue := SetKey{} // ----//---- in B for δ+ Bkqueue := SetKey{} // ----//---- in B for δ+ XXX ----//----
Akdone := SetKey{} // already processed keys in A Akdone := SetKey{} // already processed keys in A XXX ----//----
Bkdone := SetKey{} // ----//---- in B Bkdone := SetKey{} // ----//---- in B XXX ----//----
Aktodo := func(k Key) { Aktodo := func(k Key) {
if !Akdone.Has(k) { if !Akdone.Has(k) {
tracef(" Akq <- %d\n", k) tracef(" Akq <- %d\n", k)
Akqueue.Add(k) Akqueue.Add(k)
} }
} }
Bktodo := func(k Key) { Bktodo := func(lo, hi_ Key) { // XXX func(r KeyRange)
panic("TODO")
/* XXX reenable
δtodo := r.Difference(Bkdone) // XXX name
if !δtodo.Empty() {
tracef(" Bkq <- %s\n", δtodo)
Bkqueue.UnionInplace(δtodo)
}
*/
/* XXX kill
if !Bkdone.Has(k) { if !Bkdone.Has(k) {
tracef(" Bkq <- %d\n", k) tracef(" Bkq <- %d\n", k)
Bkqueue.Add(k) Bkqueue.Add(k)
} }
*/
} }
// {} oid -> parent for all nodes in Bv: current and previously expanded - up till top B // {} oid -> parent for all nodes in Bv: current and previously expanded - up till top B
...@@ -564,23 +576,30 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet, ...@@ -564,23 +576,30 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet,
err = δMerge(δ, δA); /*X*/if err != nil { return nil,nil, err } err = δMerge(δ, δA); /*X*/if err != nil { return nil,nil, err }
δtrack.Del.AddPath(ra.Path()) δtrack.Del.AddPath(ra.Path())
/*
// Bkqueue <- δA // Bkqueue <- δA
for k := range δA { for k := range δA {
Akdone.Add(k) Akdone.Add(k)
Bktodo(k) Bktodo(k)
} }
// Bkqueue <- holes(ra.range) // Bkqueue <- holes(ra.range) XXX -> Bktodo(ra.range)
for k := range holeIdx.GetInRange(ra.lo, ra.hi_) { for k := range holeIdx.GetInRange(ra.lo, ra.hi_) {
Bktodo(k) Bktodo(k)
} }
*/
// Bkqueue <- ra.range
Bktodo(ra.lo, ra.hi_)
ra.done = true ra.done = true
case *Tree: case *Tree:
// empty tree - only queue holes covered by it // empty tree - only queue holes covered by it
if len(a.Entryv()) == 0 { if len(a.Entryv()) == 0 {
Bktodo(ra.lo, ra.hi_)
/* XXX kill
for k := range holeIdx.GetInRange(ra.lo, ra.hi_) { for k := range holeIdx.GetInRange(ra.lo, ra.hi_) {
Bktodo(k) Bktodo(k)
} }
*/
continue continue
} }
...@@ -732,6 +751,9 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet, ...@@ -732,6 +751,9 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet,
err = δMerge(δ, δA); /*X*/if err != nil { return nil,nil, err } err = δMerge(δ, δA); /*X*/if err != nil { return nil,nil, err }
δtrack.Del.AddPath(a.Path()) δtrack.Del.AddPath(a.Path())
// Bkqueue <- a.range
Bktodo(a.lo, a.hi_)
/* XXX kill
// Bkqueue <- δA // Bkqueue <- δA
for k_ := range δA { for k_ := range δA {
Akdone.Add(k_) Akdone.Add(k_)
...@@ -741,6 +763,7 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet, ...@@ -741,6 +763,7 @@ func diffT(ctx context.Context, A, B *Tree, δZTC SetOid, trackSet PPTreeSubSet,
for k_ := range holeIdx.GetInRange(a.lo, a.hi_) { for k_ := range holeIdx.GetInRange(a.lo, a.hi_) {
Bktodo(k_) Bktodo(k_)
} }
*/
a.done = true a.done = true
} }
......
...@@ -102,10 +102,10 @@ type ΔBtail struct { ...@@ -102,10 +102,10 @@ type ΔBtail struct {
// set of node that were requested to be tracked, but for which vδB was not yet rebuilt // set of node that were requested to be tracked, but for which vδB was not yet rebuilt
trackNew PPTreeSubSet trackNew PPTreeSubSet
// XXX root -> tracked holes // XXX kill
// XXX move -> ΔTtail ? // // XXX root -> tracked holes
holeIdxByRoot map[zodb.Oid]treeSetKey // // XXX move -> ΔTtail ?
// holeIdxByRoot map[zodb.Oid]treeSetKey
} }
...@@ -149,7 +149,7 @@ func NewΔBtail(at0 zodb.Tid, db *zodb.DB) *ΔBtail { ...@@ -149,7 +149,7 @@ func NewΔBtail(at0 zodb.Tid, db *zodb.DB) *ΔBtail {
byRoot: map[zodb.Oid]*ΔTtail{}, byRoot: map[zodb.Oid]*ΔTtail{},
trackSet: PPTreeSubSet{}, trackSet: PPTreeSubSet{},
trackNew: PPTreeSubSet{}, trackNew: PPTreeSubSet{},
holeIdxByRoot: map[zodb.Oid]treeSetKey{}, // holeIdxByRoot: map[zodb.Oid]treeSetKey{},
db: db, db: db,
} }
} }
...@@ -184,6 +184,7 @@ func (orig *ΔBtail) clone() *ΔBtail { ...@@ -184,6 +184,7 @@ func (orig *ΔBtail) clone() *ΔBtail {
klon.byRoot[root] = klonΔTtail klon.byRoot[root] = klonΔTtail
} }
/* XXX kill
// holeIdxByRoot // holeIdxByRoot
klon.holeIdxByRoot = make(map[zodb.Oid]treeSetKey, len(orig.holeIdxByRoot)) klon.holeIdxByRoot = make(map[zodb.Oid]treeSetKey, len(orig.holeIdxByRoot))
for root, origHoleIdx := range orig.holeIdxByRoot { for root, origHoleIdx := range orig.holeIdxByRoot {
...@@ -191,6 +192,7 @@ func (orig *ΔBtail) clone() *ΔBtail { ...@@ -191,6 +192,7 @@ func (orig *ΔBtail) clone() *ΔBtail {
klonHoleIdx.Update(origHoleIdx.SetKey) klonHoleIdx.Update(origHoleIdx.SetKey)
klon.holeIdxByRoot[root] = klonHoleIdx klon.holeIdxByRoot[root] = klonHoleIdx
} }
*/
return klon return klon
} }
...@@ -241,21 +243,22 @@ func (δBtail *ΔBtail) track(key Key, keyPresent bool, path []zodb.Oid) error { ...@@ -241,21 +243,22 @@ func (δBtail *ΔBtail) track(key Key, keyPresent bool, path []zodb.Oid) error {
// track is track of path[-1] (i.e. leaf) // track is track of path[-1] (i.e. leaf)
// remember missing keys in track of leaf node (bucket or top-level ø tree) // XXX kill
holeIdx := δBtail.holeIdxFor(root) // // remember missing keys in track of leaf node (bucket or top-level ø tree)
if !keyPresent { // holeIdx := δBtail.holeIdxFor(root)
holeIdx.Add(key) // if !keyPresent {
//track.holes.Add(key) // holeIdx.Add(key)
} else { // //track.holes.Add(key)
/* // } else {
if track.holes.Has(key) { // /*
panicf("[%v] was previously requested to be tracked as ø", key) // if track.holes.Has(key) {
} // panicf("[%v] was previously requested to be tracked as ø", key)
*/ // }
if holeIdx.Has(key) { // */
panicf("[%v] was previously requested to be tracked as ø", key) // if holeIdx.Has(key) {
} // panicf("[%v] was previously requested to be tracked as ø", key)
} // }
// }
// XXX hack - until rebuild is implemented // XXX hack - until rebuild is implemented
if XXX_killWhenRebuildWorks { if XXX_killWhenRebuildWorks {
...@@ -270,6 +273,7 @@ if XXX_killWhenRebuildWorks { ...@@ -270,6 +273,7 @@ if XXX_killWhenRebuildWorks {
return nil return nil
} }
/* XXX kill
func (δBtail *ΔBtail) holeIdxFor(root zodb.Oid) treeSetKey { func (δBtail *ΔBtail) holeIdxFor(root zodb.Oid) treeSetKey {
holeIdx, ok := δBtail.holeIdxByRoot[root] holeIdx, ok := δBtail.holeIdxByRoot[root]
if !ok { if !ok {
...@@ -278,6 +282,7 @@ func (δBtail *ΔBtail) holeIdxFor(root zodb.Oid) treeSetKey { ...@@ -278,6 +282,7 @@ func (δBtail *ΔBtail) holeIdxFor(root zodb.Oid) treeSetKey {
} }
return holeIdx return holeIdx
} }
*/
// rebuild rebuilds ΔBtail taking trackNew requests into account. // rebuild rebuilds ΔBtail taking trackNew requests into account.
// XXX place // XXX place
...@@ -332,9 +337,8 @@ func (δBtail *ΔBtail) rebuild() (err error) { ...@@ -332,9 +337,8 @@ func (δBtail *ΔBtail) rebuild() (err error) {
} }
for root, δtops := range δtopsByRoot { for root, δtops := range δtopsByRoot {
holeIdx := treeSetKey{SetKey{}} // XXX stub
// diff backwards curr -> prev // diff backwards curr -> prev
δT, δtrack, err := treediff(ctx, root, δtops, δZTC, trackNew, holeIdx, zconnCurr, zconnPrev) δT, δtrack, err := treediff(ctx, root, δtops, δZTC, trackNew, zconnCurr, zconnPrev)
if err != nil { if err != nil {
return err return err
} }
...@@ -428,7 +432,7 @@ if XXX_killWhenRebuildWorks { ...@@ -428,7 +432,7 @@ if XXX_killWhenRebuildWorks {
tracef("Update @%s -> @%s\n", δBtail.Head(), δZ.Tid) tracef("Update @%s -> @%s\n", δBtail.Head(), δZ.Tid)
tracef("δZ:\t%v\n", δZ.Changev) tracef("δZ:\t%v\n", δZ.Changev)
tracef("trackSet: %v\n", δBtail.trackSet) tracef("trackSet: %v\n", δBtail.trackSet)
tracef("holeIdxByRoot: %v\n", δBtail.holeIdxByRoot) // tracef("holeIdxByRoot: %v\n", δBtail.holeIdxByRoot) XXX kill
// XXX dup wrt rebuild? // XXX dup wrt rebuild?
...@@ -457,8 +461,7 @@ if XXX_killWhenRebuildWorks { ...@@ -457,8 +461,7 @@ if XXX_killWhenRebuildWorks {
} }
for root, δtops := range δtopsByRoot { for root, δtops := range δtopsByRoot {
holeIdx := δBtail.holeIdxByRoot[root] δT, δtrack, err := treediff(ctx, root, δtops, δZTC, δBtail.trackSet, zconnOld, zconnNew)
δT, δtrack, err := treediff(ctx, root, δtops, δZTC, δBtail.trackSet, holeIdx, zconnOld, zconnNew)
if err != nil { if err != nil {
return ΔB{}, err return ΔB{}, err
} }
......
...@@ -303,6 +303,7 @@ func (rbs RBucketSet) coverage() string { ...@@ -303,6 +303,7 @@ func (rbs RBucketSet) coverage() string {
return s return s
} }
/* XXX kill
// holeIdx returns what should be ΔBtree.holeIdx for specified tracked key set. // holeIdx returns what should be ΔBtree.holeIdx for specified tracked key set.
func (rbs RBucketSet) holeIdx(tracked SetKey) SetKey { func (rbs RBucketSet) holeIdx(tracked SetKey) SetKey {
holes := SetKey{} holes := SetKey{}
...@@ -314,6 +315,7 @@ func (rbs RBucketSet) holeIdx(tracked SetKey) SetKey { ...@@ -314,6 +315,7 @@ func (rbs RBucketSet) holeIdx(tracked SetKey) SetKey {
} }
return holes return holes
} }
*/
// trackSet returns what should be ΔBtree.trackSet for specified tracked key set. // trackSet returns what should be ΔBtree.trackSet for specified tracked key set.
func (rbs RBucketSet) trackSet(tracked SetKey) PPTreeSubSet { func (rbs RBucketSet) trackSet(tracked SetKey) PPTreeSubSet {
...@@ -714,6 +716,7 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -714,6 +716,7 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
} }
} }
/* XXX kill
// verify δbtail.holeIdx against @at1 // verify δbtail.holeIdx against @at1
// holes1 = tracked1 \ kv1 // holes1 = tracked1 \ kv1
holes1 := xkv1.holeIdx(initialTrackedKeys) holes1 := xkv1.holeIdx(initialTrackedKeys)
...@@ -721,6 +724,7 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -721,6 +724,7 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
if !reflect.DeepEqual(holes1, holeIdx.SetKey) { if !reflect.DeepEqual(holes1, holeIdx.SetKey) {
badf("δbtail.holeIdx1 wrong ; holeIdx=%v holeIdxOK=%v", holeIdx, holes1) badf("δbtail.holeIdx1 wrong ; holeIdx=%v holeIdxOK=%v", holeIdx, holes1)
} }
*/
ø := PPTreeSubSet{} ø := PPTreeSubSet{}
...@@ -746,12 +750,14 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -746,12 +750,14 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
return return
} }
/* XXX kill
// verify δbtail.holeIdx against @at2 // verify δbtail.holeIdx against @at2
// holes2 = tracked2 \ kv2 ( = kadj[tracked1] \ kv2) // holes2 = tracked2 \ kv2 ( = kadj[tracked1] \ kv2)
holes2 := xkv2.holeIdx(kadjTracked) holes2 := xkv2.holeIdx(kadjTracked)
if !reflect.DeepEqual(holes2, holeIdx.SetKey) { if !reflect.DeepEqual(holes2, holeIdx.SetKey) {
badf("δbtail.holeIdx2 wrong ; holeIdx=%v holeIdxOK=%v", holeIdx, holes2) badf("δbtail.holeIdx2 wrong ; holeIdx=%v holeIdxOK=%v", holeIdx, holes2)
} }
*/
// verify δbtail.trackSet against @at2 // verify δbtail.trackSet against @at2
// trackSet2 = xkv2[tracked2] ( = xkv2[kadj[tracked1]] // trackSet2 = xkv2[tracked2] ( = xkv2[kadj[tracked1]]
...@@ -841,8 +847,6 @@ func (δbtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK, track ...@@ -841,8 +847,6 @@ func (δbtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK, track
t.Helper() t.Helper()
assertTrack(t, subj + ": trackSet", δbtail.trackSet, trackSetOK) assertTrack(t, subj + ": trackSet", δbtail.trackSet, trackSetOK)
assertTrack(t, subj + ": trackNew", δbtail.trackNew, trackNewOK) assertTrack(t, subj + ": trackNew", δbtail.trackNew, trackNewOK)
// XXX also verify .holeIdx XXX or better get rid of .holeIdx
} }
// xverifyΔBTail_rebuild verifies δBtail.rebuild during t0->t1->t2 transition. // xverifyΔBTail_rebuild verifies δBtail.rebuild during t0->t1->t2 transition.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment