obsolete faster by speeding up low-mem versions
This commit is contained in:
parent
c2a84f98b9
commit
f06ff12ed5
|
@ -0,0 +1,11 @@
|
||||||
|
equi
|
||||||
|
equi1
|
||||||
|
equi1g
|
||||||
|
faster
|
||||||
|
faster1
|
||||||
|
equi965
|
||||||
|
equi1445
|
||||||
|
eqcuda
|
||||||
|
eqcuda1445
|
||||||
|
feqcuda
|
||||||
|
verify
|
|
@ -3,9 +3,7 @@ The MIT License (MIT)
|
||||||
Copyright (c) 2016 John Tromp
|
Copyright (c) 2016 John Tromp
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software, EXCEPT FOR blake2b.cu WHICH ORIGINATES FROM
|
of this software, and associated documentation files (the "Software"), to deal
|
||||||
https://github.com/tpruvot/ccminer/blob/windows/sia/sia.cu,
|
|
||||||
and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
|
20
Makefile
20
Makefile
|
@ -2,23 +2,17 @@ OPT = -O3
|
||||||
FLAGS = -Wall -Wno-deprecated-declarations -D_POSIX_C_SOURCE=200112L $(OPT) -pthread
|
FLAGS = -Wall -Wno-deprecated-declarations -D_POSIX_C_SOURCE=200112L $(OPT) -pthread
|
||||||
GPP = g++ -march=native -m64 -std=c++11 $(FLAGS)
|
GPP = g++ -march=native -m64 -std=c++11 $(FLAGS)
|
||||||
|
|
||||||
all: equi equi1 faster faster1 verify test spark
|
all: equi equi1 verify test spark
|
||||||
|
|
||||||
equi: equi.h equi_miner.h equi_miner.cpp Makefile
|
equi: equi.h equi_miner.h equi_miner.cpp Makefile
|
||||||
$(GPP) -DATOMIC equi_miner.cpp blake/blake2b.cpp -o equi
|
$(GPP) -DATOMIC equi_miner.cpp blake/blake2b.cpp -o equi
|
||||||
|
|
||||||
equi1: equi.h equi_miner.h equi_miner.cpp Makefile
|
equi1: equi.h equi_miner.h equi_miner.cpp Makefile
|
||||||
$(GPP) -DSPARK equi_miner.cpp blake/blake2b.cpp -o equi1
|
$(GPP) equi_miner.cpp blake/blake2b.cpp -o equi1
|
||||||
|
|
||||||
equi1g: equi.h equi_miner.h equi_miner.cpp Makefile
|
equi1g: equi.h equi_miner.h equi_miner.cpp Makefile
|
||||||
g++ -g -DSPARK equi_miner.cpp blake/blake2b.cpp -pthread -o equi1g
|
g++ -g -DSPARK equi_miner.cpp blake/blake2b.cpp -pthread -o equi1g
|
||||||
|
|
||||||
faster: equi.h equi_miner.h equi_miner.cpp Makefile
|
|
||||||
$(GPP) -DJOINHT -DATOMIC equi_miner.cpp blake/blake2b.cpp -o faster
|
|
||||||
|
|
||||||
faster1: equi.h equi_miner.h equi_miner.cpp Makefile
|
|
||||||
$(GPP) -DJOINHT equi_miner.cpp blake/blake2b.cpp -o faster1
|
|
||||||
|
|
||||||
equi965: equi.h equi_miner.h equi_miner.cpp Makefile
|
equi965: equi.h equi_miner.h equi_miner.cpp Makefile
|
||||||
$(GPP) -DWN=96 -DWK=5 equi_miner.cpp blake/blake2b.cpp -o equi965
|
$(GPP) -DWN=96 -DWK=5 equi_miner.cpp blake/blake2b.cpp -o equi965
|
||||||
|
|
||||||
|
@ -37,14 +31,14 @@ feqcuda: equi_miner.cu equi.h blake2b.cu Makefile
|
||||||
verify: equi.h equi.c Makefile
|
verify: equi.h equi.c Makefile
|
||||||
g++ -g equi.c blake/blake2b.cpp -o verify
|
g++ -g equi.c blake/blake2b.cpp -o verify
|
||||||
|
|
||||||
bench: equi
|
bench: equi1
|
||||||
time for i in {0..9}; do ./faster -n $$i; done
|
time ./equi1 -n 1000 -r 100
|
||||||
|
|
||||||
test: equi verify Makefile
|
test: equi verify Makefile
|
||||||
time ./equi -h "" -n 0 -t 1 -s | grep ^Sol | ./verify -h "" -n 0
|
time ./equi -h "" -n 0 -t 1 -s | grep ^Sol | ./verify -h "" -n 0
|
||||||
|
|
||||||
spark: equi1
|
spark: equi1g
|
||||||
time ./equi1
|
time ./equi1g
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm equi equi1 equi1g faster faster1 equi965 equi1445 eqcuda eqcuda1445 feqcuda verify
|
rm equi equi1 equi1g equi965 equi1445 eqcuda eqcuda1445 feqcuda verify
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// Blake2-B CUDA Implementation
|
// Blake2-B CUDA Implementation
|
||||||
// tpruvot@github July 2016
|
// tpruvot@github July 2016
|
||||||
|
// permission granted to use under MIT license
|
||||||
|
// modified for use in Zcash by John Tromp September 2016
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uint2 direct ops by c++ operator definitions
|
* uint2 direct ops by c++ operator definitions
|
||||||
|
|
|
@ -43,10 +43,11 @@ int main(int argc, char **argv) {
|
||||||
printf("Looking for wagner-tree on (\"%s\",%d", header, nonce);
|
printf("Looking for wagner-tree on (\"%s\",%d", header, nonce);
|
||||||
if (range > 1)
|
if (range > 1)
|
||||||
printf("-%d", nonce+range-1);
|
printf("-%d", nonce+range-1);
|
||||||
printf(") with %d %d-bits digits and %d threads\n", NDIGITS, DIGITBITS, nthreads);
|
printf(") with %d %d-bit digits and %d threads\n", NDIGITS, DIGITBITS, nthreads);
|
||||||
thread_ctx *threads = (thread_ctx *)calloc(nthreads, sizeof(thread_ctx));
|
thread_ctx *threads = (thread_ctx *)calloc(nthreads, sizeof(thread_ctx));
|
||||||
assert(threads);
|
assert(threads);
|
||||||
equi eq(nthreads);
|
equi eq(nthreads);
|
||||||
|
printf("Using %dMB of memory\n", eq.hta.alloced >> 20);
|
||||||
u32 sumnsols = 0;
|
u32 sumnsols = 0;
|
||||||
for (int r = 0; r < range; r++) {
|
for (int r = 0; r < range; r++) {
|
||||||
eq.setnonce(header, nonce+r);
|
eq.setnonce(header, nonce+r);
|
||||||
|
|
137
equi_miner.h
137
equi_miner.h
|
@ -109,87 +109,52 @@ u32 htunits(u32 bytes) {
|
||||||
return (bytes + sizeof(htunit) - 1) / sizeof(htunit);
|
return (bytes + sizeof(htunit) - 1) / sizeof(htunit);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef JOINHT
|
|
||||||
u32 slotsize(const u32 r) {
|
|
||||||
return 1 + htunits(hashsize(r));
|
|
||||||
}
|
|
||||||
// size (in htunits) of bucket in round 0 <= r < WK
|
|
||||||
u32 bucketsize(const u32 r) {
|
|
||||||
return NSLOTS * slotsize(r);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
u32 slotsize(const u32 r) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// manages hash and tree data
|
// manages hash and tree data
|
||||||
struct htalloc {
|
struct htalloc {
|
||||||
// Defining JOINHT joins each tree with its corresponding hash,
|
|
||||||
// so they may share a cache line. This gives a small speed
|
|
||||||
// advantage but comes at the cost of a big memory increase
|
|
||||||
// as hash-space can no longer be reclaimed
|
|
||||||
#ifdef JOINHT
|
|
||||||
htunit *trees[WK];
|
htunit *trees[WK];
|
||||||
#else
|
u32 alloced;
|
||||||
bucket *trees[WK];
|
|
||||||
htunit *hashes[WK];
|
|
||||||
#endif
|
|
||||||
u64 alloced;
|
|
||||||
htalloc() {
|
htalloc() {
|
||||||
alloced = 0;
|
alloced = 0;
|
||||||
}
|
}
|
||||||
void alloctrees() {
|
void alloctrees() {
|
||||||
#ifdef JOINHT
|
|
||||||
for (int r=0; r<WK; r++)
|
|
||||||
trees[r] = (htunit *)alloc(NBUCKETS * NSLOTS * (1 + htunits(hashsize(r))), sizeof(htunit));
|
|
||||||
#else
|
|
||||||
// optimize xenoncat's fixed memory layout, avoiding any waste
|
// optimize xenoncat's fixed memory layout, avoiding any waste
|
||||||
// digit trees hashes trees
|
// digit trees hashes trees hashes
|
||||||
// 0 0 A A A A A A . . . . . .
|
// 0 0 A A A A A A . . . . . .
|
||||||
// 1 0 A A A A A A B B B B B 1
|
// 1 0 A A A A A A 1 B B B B B
|
||||||
// 2 0 2 C C C C C B B B B B 1
|
// 2 0 2 C C C C C 1 B B B B B
|
||||||
// 3 0 2 C C C C C D D D D 3 1
|
// 3 0 2 C C C C C 1 3 D D D D
|
||||||
// 4 0 2 4 E E E E D D D D 3 1
|
// 4 0 2 4 E E E E 1 3 D D D D
|
||||||
// 5 0 2 4 E E E E F F F 5 3 1
|
// 5 0 2 4 E E E E 1 3 5 F F F
|
||||||
// 6 0 2 4 6 . G G F F F 5 3 1
|
// 6 0 2 4 6 . G G 1 3 5 F F F
|
||||||
// 7 0 2 4 6 . G G H H 7 5 3 1
|
// 7 0 2 4 6 . G G 1 3 5 7 H H
|
||||||
// 8 0 2 4 6 8 . I H H 7 5 3 1
|
// 8 0 2 4 6 8 . I 1 3 5 7 H H
|
||||||
assert(DIGITBITS >= 16); // ensures hashes shorten by 1 unit every 2 digits
|
assert(DIGITBITS >= 16); // ensures hashes shorten by 1 unit every 2 digits
|
||||||
u32 units0 = htunits(hashsize(0)), units1 = htunits(hashsize(1));
|
digit *heap[2];
|
||||||
digit *heap = (digit *)alloc(1+units0+units1+1, sizeof(digit));
|
for (u32 i =0; i < 2; i++)
|
||||||
for (int r=0; r<WK; r++) {
|
heap[i] = (digit *)alloc(1 + htunits(hashsize(i)), sizeof(digit));
|
||||||
trees[r] = (bucket *)(heap + (r&1 ? 1+units0+units1-r/2 : r/2));
|
for (int r=0; r<WK; r++)
|
||||||
hashes[r] = (htunit *)(heap + (r&1 ? 1+units0 : 1+r/2));
|
trees[r] = (htunit *)heap[r&1] + r/2;
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
void dealloctrees() {
|
void dealloctrees() {
|
||||||
#ifdef JOINHT
|
for (u32 i =0; i < 2; i++)
|
||||||
for (int r=0; r<WK; r++)
|
free(trees[i]);
|
||||||
dealloc(trees[r], NBUCKETS * NSLOTS * (1 + htunits(hashsize(r))), sizeof(htunit));
|
}
|
||||||
#else
|
u32 slotsize(const u32 r) const {
|
||||||
u32 units0 = htunits(hashsize(0)), units1 = htunits(hashsize(1));
|
return 1 + htunits(hashsize(r&1));
|
||||||
dealloc(trees[0], 1+units0+units1+1, sizeof(digit));
|
}
|
||||||
#endif
|
// size (in htunits) of bucket in round 0 <= r < WK
|
||||||
|
u32 bucketsize(const u32 r) const {
|
||||||
|
return NSLOTS * slotsize(r);
|
||||||
}
|
}
|
||||||
htunit *getbucket(u32 r, u32 bid) const {
|
htunit *getbucket(u32 r, u32 bid) const {
|
||||||
#ifdef JOINHT
|
|
||||||
return &trees[r][bid * bucketsize(r)];
|
return &trees[r][bid * bucketsize(r)];
|
||||||
#else
|
|
||||||
return trees[r][bid];
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
void *alloc(const u32 n, const u32 sz) {
|
void *alloc(const u32 n, const u32 sz) {
|
||||||
void *mem = calloc(n, sz);
|
void *mem = calloc(n, sz);
|
||||||
assert(mem);
|
assert(mem);
|
||||||
alloced += (u64)n * sz;
|
alloced += n * sz;
|
||||||
return mem;
|
return mem;
|
||||||
}
|
}
|
||||||
void dealloc(void *mem, const u32 n, const u32 sz) {
|
|
||||||
free(mem);
|
|
||||||
alloced -= (u64)n * sz;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef au32 bsizes[NBUCKETS];
|
typedef au32 bsizes[NBUCKETS];
|
||||||
|
@ -249,8 +214,8 @@ struct equi {
|
||||||
const htunit *bt = hta.getbucket(--r,t.bucketid);
|
const htunit *bt = hta.getbucket(--r,t.bucketid);
|
||||||
const u32 size = 1 << r;
|
const u32 size = 1 << r;
|
||||||
u32 *indices1 = indices + size;
|
u32 *indices1 = indices + size;
|
||||||
listindices(r, bt[t.slotid0 * slotsize(r)].attr, indices);
|
listindices(r, bt[t.slotid0 * hta.slotsize(r)].attr, indices);
|
||||||
listindices(r, bt[t.slotid1 * slotsize(r)].attr, indices1);
|
listindices(r, bt[t.slotid1 * hta.slotsize(r)].attr, indices1);
|
||||||
if (*indices > *indices1) {
|
if (*indices > *indices1) {
|
||||||
for (u32 i=0; i < size; i++) {
|
for (u32 i=0; i < size; i++) {
|
||||||
const u32 tmp = indices[i];
|
const u32 tmp = indices[i];
|
||||||
|
@ -292,7 +257,7 @@ struct equi {
|
||||||
printf("\342\226%c", '\201'+bsizes[i]/SPARKSCALE);
|
printf("\342\226%c", '\201'+bsizes[i]/SPARKSCALE);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
printf(" %ld MB\n", hta.alloced >> 20);
|
printf("\n");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,65 +265,47 @@ struct equi {
|
||||||
htalloc hta;
|
htalloc hta;
|
||||||
u32 prevhtunits;
|
u32 prevhtunits;
|
||||||
u32 nexthtunits;
|
u32 nexthtunits;
|
||||||
|
u32 prevslotunits;
|
||||||
|
u32 nextslotunits;
|
||||||
u32 dunits;
|
u32 dunits;
|
||||||
u32 prevbo;
|
u32 prevbo;
|
||||||
u32 nextbo;
|
u32 nextbo;
|
||||||
htunit *buck;
|
htunit *buck;
|
||||||
htunit *hashbase;
|
htunit *hashbase;
|
||||||
|
|
||||||
htlayout(equi *eq, u32 r): hta(eq->hta), prevhtunits(0), dunits(0) {
|
htlayout(equi *eq, u32 r): hta(eq->hta), prevhtunits(0), prevslotunits(0), dunits(0) {
|
||||||
u32 nexthashbytes = hashsize(r);
|
u32 nexthashbytes = hashsize(r);
|
||||||
nexthtunits = htunits(nexthashbytes);
|
nexthtunits = htunits(nexthashbytes);
|
||||||
|
nextslotunits = 1 + htunits(hashsize(r&1));
|
||||||
prevbo = 0;
|
prevbo = 0;
|
||||||
nextbo = nexthtunits * sizeof(htunit) - nexthashbytes; // 0-3
|
nextbo = nexthtunits * sizeof(htunit) - nexthashbytes; // 0-3
|
||||||
if (r) {
|
if (r) {
|
||||||
u32 prevhashbytes = hashsize(r-1);
|
u32 prevhashbytes = hashsize(r-1);
|
||||||
prevhtunits = htunits(prevhashbytes);
|
prevhtunits = htunits(prevhashbytes);
|
||||||
|
prevslotunits = 1 + htunits(hashsize((r-1)&1));
|
||||||
prevbo = prevhtunits * sizeof(htunit) - prevhashbytes; // 0-3
|
prevbo = prevhtunits * sizeof(htunit) - prevhashbytes; // 0-3
|
||||||
dunits = prevhtunits - nexthtunits;
|
dunits = prevhtunits - nexthtunits;
|
||||||
}
|
}
|
||||||
#ifdef JOINHT
|
|
||||||
nexthtunits++;
|
|
||||||
prevhtunits++;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
void setbucket(u32 r, u32 bid) {
|
void setbucket(u32 r, u32 bid) {
|
||||||
buck = hta.getbucket(r, bid);
|
buck = hta.getbucket(r, bid);
|
||||||
#ifdef JOINHT
|
|
||||||
hashbase = buck + 1;
|
hashbase = buck + 1;
|
||||||
#else
|
|
||||||
hashbase = hta.hashes[r] + (bid * NSLOTS) * prevhtunits;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
u32 getxhash(const u32 slot, const htunit *hash) const {
|
u32 getxhash(const u32 slot, const htunit *hash) const {
|
||||||
#ifdef XWITHASH
|
#ifdef XWITHASH
|
||||||
return hash->bytes[prevbo] & 0xf;
|
return hash->bytes[prevbo] & 0xf;
|
||||||
#elif defined JOINHT
|
|
||||||
return buck[slot * prevhtunits].attr.xhash;
|
|
||||||
#else
|
#else
|
||||||
return buck[slot].attr.xhash;
|
return buck[slot * prevslotunits].attr.xhash;
|
||||||
#endif
|
|
||||||
}
|
|
||||||
u32 prevhashunits() const {
|
|
||||||
#ifdef JOINHT
|
|
||||||
return prevhtunits - 1;
|
|
||||||
#else
|
|
||||||
return prevhtunits;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
bool equal(const htunit *hash0, const htunit *hash1) const {
|
bool equal(const htunit *hash0, const htunit *hash1) const {
|
||||||
return hash0[prevhashunits()-1].hash == hash1[prevhashunits()-1].hash;
|
return hash0[prevhtunits-1].hash == hash1[prevhtunits-1].hash;
|
||||||
}
|
}
|
||||||
htunit *addtree(u32 r, tree t, u32 bid, u32 slot) {
|
htunit *addtree(u32 r, tree t, u32 bid, u32 slot) {
|
||||||
htunit *buck = hta.getbucket(r,bid);
|
htunit *buck = hta.getbucket(r,bid);
|
||||||
#ifdef JOINHT
|
htunit *slotree = buck + slot * nextslotunits;
|
||||||
htunit *slotree = buck + slot * nexthtunits;
|
|
||||||
slotree->attr = t;
|
slotree->attr = t;
|
||||||
return slotree + 1;
|
return slotree + 1;
|
||||||
#else
|
|
||||||
buck[slot].attr = t;
|
|
||||||
return hta.hashes[r] + (bid * NSLOTS + slot) * nexthtunits;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -467,14 +414,14 @@ struct equi {
|
||||||
htl.setbucket(r-1, bucketid);
|
htl.setbucket(r-1, bucketid);
|
||||||
u32 bsize = getnslots(r-1, bucketid);
|
u32 bsize = getnslots(r-1, bucketid);
|
||||||
for (u32 s1 = 0; s1 < bsize; s1++) {
|
for (u32 s1 = 0; s1 < bsize; s1++) {
|
||||||
const htunit *hash1 = htl.hashbase + s1 * htl.prevhtunits;
|
const htunit *hash1 = htl.hashbase + s1 * htl.prevslotunits;
|
||||||
if (!cd.addslot(s1, htl.getxhash(s1, hash1))) {
|
if (!cd.addslot(s1, htl.getxhash(s1, hash1))) {
|
||||||
xfull++;
|
xfull++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (; cd.nextcollision(); ) {
|
for (; cd.nextcollision(); ) {
|
||||||
const u32 s0 = cd.slot();
|
const u32 s0 = cd.slot();
|
||||||
const htunit *hash0 = htl.hashbase + s0 * htl.prevhtunits;
|
const htunit *hash0 = htl.hashbase + s0 * htl.prevslotunits;
|
||||||
if (htl.equal(hash0, hash1)) {
|
if (htl.equal(hash0, hash1)) {
|
||||||
hfull++;
|
hfull++;
|
||||||
continue;
|
continue;
|
||||||
|
@ -511,7 +458,7 @@ struct equi {
|
||||||
xort.xhash = xhash;
|
xort.xhash = xhash;
|
||||||
#endif
|
#endif
|
||||||
htunit *xorhash = htl.addtree(r, xort, xorbucketid, xorslot);
|
htunit *xorhash = htl.addtree(r, xort, xorbucketid, xorslot);
|
||||||
for (u32 i=htl.dunits; i < htl.prevhashunits(); i++)
|
for (u32 i=htl.dunits; i < htl.prevhtunits; i++)
|
||||||
xorhash[i-htl.dunits].hash = hash0[i].hash ^ hash1[i].hash;
|
xorhash[i-htl.dunits].hash = hash0[i].hash ^ hash1[i].hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,12 +473,12 @@ struct equi {
|
||||||
htl.setbucket(WK-1, bucketid);
|
htl.setbucket(WK-1, bucketid);
|
||||||
u32 bsize = getnslots(WK-1, bucketid);
|
u32 bsize = getnslots(WK-1, bucketid);
|
||||||
for (u32 s1 = 0; s1 < bsize; s1++) {
|
for (u32 s1 = 0; s1 < bsize; s1++) {
|
||||||
const htunit *hash1 = htl.hashbase + s1 * htl.prevhtunits;
|
const htunit *hash1 = htl.hashbase + s1 * htl.prevslotunits;
|
||||||
if (!cd.addslot(s1, htl.getxhash(s1, hash1)))
|
if (!cd.addslot(s1, htl.getxhash(s1, hash1)))
|
||||||
continue;
|
continue;
|
||||||
for (; cd.nextcollision(); ) {
|
for (; cd.nextcollision(); ) {
|
||||||
const u32 s0 = cd.slot();
|
const u32 s0 = cd.slot();
|
||||||
const htunit *hash0 = htl.hashbase + s0 * htl.prevhtunits;
|
const htunit *hash0 = htl.hashbase + s0 * htl.prevslotunits;
|
||||||
if (htl.equal(hash0, hash1)) {
|
if (htl.equal(hash0, hash1)) {
|
||||||
tree xort; xort.bucketid = bucketid;
|
tree xort; xort.bucketid = bucketid;
|
||||||
xort.slotid0 = s0; xort.slotid1 = s1;
|
xort.slotid0 = s0; xort.slotid1 = s1;
|
||||||
|
|
Loading…
Reference in New Issue