Bläddra i källkod

Merge pull request #957 from Lezek123/pioneer-proposals-optimalization

Pioneer: Runtime upgrade proposal optimalization
Mokhtar Naamani 4 år sedan
förälder
incheckning
2a70057e2b

+ 4 - 8
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -3,7 +3,6 @@ import { Link } from 'react-router-dom';
 import { Card, Header, Button, Icon, Message } from 'semantic-ui-react';
 import { ProposalType } from '@polkadot/joy-utils/types/proposals';
 import { bytesToString } from '@polkadot/joy-utils/functions/misc';
-import { blake2AsHex } from '@polkadot/util-crypto';
 import styled from 'styled-components';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
 import TxButton from '@polkadot/joy-utils/TxButton';
@@ -110,13 +109,10 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = {
       true
     )
   ],
-  RuntimeUpgrade: ([wasm]) => {
-    const buffer: Buffer = Buffer.from(wasm.replace('0x', ''), 'hex');
-    return [
-      new ParsedParam('Blake2b256 hash of WASM code', blake2AsHex(buffer, 256), true),
-      new ParsedParam('File size', buffer.length + ' bytes')
-    ];
-  },
+  RuntimeUpgrade: ([hash, filesize]) => [
+    new ParsedParam('Blake2b256 hash of WASM code', hash, true),
+    new ParsedParam('File size', filesize + ' bytes')
+  ],
   SetElectionParameters: ([params]) => [
     new ParsedParam('Announcing period', params.announcing_period + ' blocks'),
     new ParsedParam('Voting period', params.voting_period + ' blocks'),

+ 2 - 1
pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx

@@ -3,6 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
 import ProposalDetails from './ProposalDetails';
 import { useProposalSubscription } from '@polkadot/joy-utils/react/hooks';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import { ProposalId } from '@joystream/types/proposals';
 
 export default function ProposalFromId (props: RouteComponentProps<any>) {
   const {
@@ -11,7 +12,7 @@ export default function ProposalFromId (props: RouteComponentProps<any>) {
     }
   } = props;
 
-  const { proposal: proposalState, votes: votesState } = useProposalSubscription(id);
+  const { proposal: proposalState, votes: votesState } = useProposalSubscription(new ProposalId(id));
 
   return (
     <PromiseComponent

+ 4 - 3
pioneer/packages/joy-utils/src/react/context/transport.tsx

@@ -1,4 +1,4 @@
-import React, { createContext, useContext } from 'react';
+import React, { createContext, useContext, useState } from 'react';
 import { ApiContext } from '@polkadot/react-api';
 import { ApiProps } from '@polkadot/react-api/types';
 
@@ -15,7 +15,8 @@ export function TransportProvider ({ children }: { children: React.PropsWithChil
     throw new Error('Cannot create Transport: The Substrate API is not ready yet.');
   }
 
-  const transport = new Transport(api.api);
+  // Preserve transport instance in state to allow more effective caching
+  const [transportInstance] = useState<Transport>(() => new Transport(api.api));
 
-  return <TransportContext.Provider value={transport}>{children}</TransportContext.Provider>;
+  return <TransportContext.Provider value={transportInstance}>{children}</TransportContext.Provider>;
 }

+ 16 - 13
pioneer/packages/joy-utils/src/transport/base.ts

@@ -1,59 +1,62 @@
 import { ApiPromise } from '@polkadot/api';
 import { Codec } from '@polkadot/types/types';
+import { APIQueryCache } from '../APIQueryCache';
 
 export default abstract class BaseTransport {
   protected api: ApiPromise;
+  protected cacheApi: APIQueryCache;
 
-  constructor (api: ApiPromise) {
+  constructor (api: ApiPromise, cacheApi: APIQueryCache) {
     this.api = api;
+    this.cacheApi = cacheApi;
   }
 
   protected get proposalsEngine () {
-    return this.api.query.proposalsEngine;
+    return this.cacheApi.query.proposalsEngine;
   }
 
   protected get proposalsCodex () {
-    return this.api.query.proposalsCodex;
+    return this.cacheApi.query.proposalsCodex;
   }
 
   protected get proposalsDiscussion () {
-    return this.api.query.proposalsDiscussion;
+    return this.cacheApi.query.proposalsDiscussion;
   }
 
   protected get members () {
-    return this.api.query.members;
+    return this.cacheApi.query.members;
   }
 
   protected get council () {
-    return this.api.query.council;
+    return this.cacheApi.query.council;
   }
 
   protected get councilElection () {
-    return this.api.query.councilElection;
+    return this.cacheApi.query.councilElection;
   }
 
   protected get actors () {
-    return this.api.query.actors;
+    return this.cacheApi.query.actors;
   }
 
   protected get contentWorkingGroup () {
-    return this.api.query.contentWorkingGroup;
+    return this.cacheApi.query.contentWorkingGroup;
   }
 
   protected get minting () {
-    return this.api.query.minting;
+    return this.cacheApi.query.minting;
   }
 
   protected get hiring () {
-    return this.api.query.hiring;
+    return this.cacheApi.query.hiring;
   }
 
   protected get stake () {
-    return this.api.query.stake;
+    return this.cacheApi.query.stake;
   }
 
   protected get recurringRewards () {
-    return this.api.query.recurringRewards;
+    return this.cacheApi.query.recurringRewards;
   }
 
   protected queryMethodByName (name: string) {

+ 3 - 2
pioneer/packages/joy-utils/src/transport/contentWorkingGroup.ts

@@ -5,12 +5,13 @@ import { MintId, Mint } from '@joystream/types/mint';
 import { LeadId } from '@joystream/types/content-working-group';
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
+import { APIQueryCache } from '../APIQueryCache';
 
 export default class ContentWorkingGroupTransport extends BaseTransport {
   private membersT: MembersTransport;
 
-  constructor (api: ApiPromise, membersTransport: MembersTransport) {
-    super(api);
+  constructor (api: ApiPromise, cacheApi: APIQueryCache, membersTransport: MembersTransport) {
+    super(api, cacheApi);
     this.membersT = membersTransport;
   }
 

+ 4 - 3
pioneer/packages/joy-utils/src/transport/council.ts

@@ -8,13 +8,14 @@ import { FIRST_MEMBER_ID } from '../consts/members';
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
 import ChainTransport from './chain';
+import { APIQueryCache } from '../APIQueryCache';
 
 export default class CouncilTransport extends BaseTransport {
   private membersT: MembersTransport;
   private chainT: ChainTransport;
 
-  constructor (api: ApiPromise, membersTransport: MembersTransport, chainTransport: ChainTransport) {
-    super(api);
+  constructor (api: ApiPromise, cacheApi: APIQueryCache, membersTransport: MembersTransport, chainTransport: ChainTransport) {
+    super(api, cacheApi);
     this.membersT = membersTransport;
     this.chainT = chainTransport;
   }
@@ -22,7 +23,7 @@ export default class CouncilTransport extends BaseTransport {
   async councilMembersLength (atBlock?: number): Promise<number> {
     if (atBlock) {
       const blockHash = await this.chainT.blockHash(atBlock);
-      return ((await this.council.activeCouncil.at(blockHash)) as Seats).length;
+      return ((await this.api.query.council.activeCouncil.at(blockHash)) as Seats).length;
     }
 
     return ((await this.council.activeCouncil()) as Seats).length;

+ 11 - 8
pioneer/packages/joy-utils/src/transport/index.ts

@@ -7,9 +7,11 @@ import CouncilTransport from './council';
 import StorageProvidersTransport from './storageProviders';
 import ValidatorsTransport from './validators';
 import WorkingGroupsTransport from './workingGroups';
+import { APIQueryCache } from '../APIQueryCache';
 
 export default class Transport {
   protected api: ApiPromise;
+  protected cacheApi: APIQueryCache;
   // Specific transports
   public chain: ChainTransport;
   public members: MembersTransport;
@@ -22,13 +24,14 @@ export default class Transport {
 
   constructor (api: ApiPromise) {
     this.api = api;
-    this.chain = new ChainTransport(api);
-    this.members = new MembersTransport(api);
-    this.storageProviders = new StorageProvidersTransport(api);
-    this.validators = new ValidatorsTransport(api);
-    this.council = new CouncilTransport(api, this.members, this.chain);
-    this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.members);
-    this.proposals = new ProposalsTransport(api, this.members, this.chain, this.council);
-    this.workingGroups = new WorkingGroupsTransport(api, this.members);
+    this.cacheApi = new APIQueryCache(api);
+    this.chain = new ChainTransport(api, this.cacheApi);
+    this.members = new MembersTransport(api, this.cacheApi);
+    this.storageProviders = new StorageProvidersTransport(api, this.cacheApi);
+    this.validators = new ValidatorsTransport(api, this.cacheApi);
+    this.council = new CouncilTransport(api, this.cacheApi, this.members, this.chain);
+    this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.cacheApi, this.members);
+    this.proposals = new ProposalsTransport(api, this.cacheApi, this.members, this.chain, this.council);
+    this.workingGroups = new WorkingGroupsTransport(api, this.cacheApi, this.members);
   }
 }

+ 51 - 18
pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -13,9 +13,9 @@ import { ParsedMember } from '../types/members';
 import BaseTransport from './base';
 
 import { ThreadId, PostId } from '@joystream/types/common';
-import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost } from '@joystream/types/proposals';
+import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails } from '@joystream/types/proposals';
 import { MemberId } from '@joystream/types/members';
-import { u32, u64 } from '@polkadot/types/';
+import { u32, u64, Bytes, Vec } from '@polkadot/types/';
 import { BalanceOf } from '@polkadot/types/interfaces';
 
 import { bytesToString } from '../functions/misc';
@@ -28,43 +28,80 @@ import MembersTransport from './members';
 import ChainTransport from './chain';
 import CouncilTransport from './council';
 
+import { blake2AsHex } from '@polkadot/util-crypto';
+import { APIQueryCache } from '../APIQueryCache';
+
+type ProposalDetailsCacheEntry = {
+  type: ProposalType;
+  details: any[];
+}
+type ProposalDetailsCache = {
+  [id: number]: ProposalDetailsCacheEntry | undefined;
+}
+
 export default class ProposalsTransport extends BaseTransport {
   private membersT: MembersTransport;
   private chainT: ChainTransport;
   private councilT: CouncilTransport;
+  private proposalDetailsCache: ProposalDetailsCache = {};
 
   constructor (
     api: ApiPromise,
+    cacheApi: APIQueryCache,
     membersTransport: MembersTransport,
     chainTransport: ChainTransport,
     councilTransport: CouncilTransport
   ) {
-    super(api);
+    super(api, cacheApi);
     this.membersT = membersTransport;
     this.chainT = chainTransport;
     this.councilT = councilTransport;
   }
 
   proposalCount () {
-    return this.proposalsEngine.proposalCount<u32>();
+    return this.proposalsEngine.proposalCount() as Promise<u32>;
   }
 
   rawProposalById (id: ProposalId) {
-    return this.proposalsEngine.proposals<Proposal>(id);
+    return this.proposalsEngine.proposals(id) as Promise<Proposal>;
   }
 
-  proposalDetailsById (id: ProposalId) {
-    return this.proposalsCodex.proposalDetailsByProposalId(id);
+  rawProposalDetails (id: ProposalId) {
+    return this.proposalsCodex.proposalDetailsByProposalId(id) as Promise<ProposalDetails>;
   }
 
   cancellationFee (): number {
     return (this.api.consts.proposalsEngine.cancellationFee as BalanceOf).toNumber();
   }
 
+  async typeAndDetails (id: ProposalId) {
+    const cachedProposalDetails = this.proposalDetailsCache[id.toNumber()];
+    // Avoid fetching runtime upgrade proposal details if we already have them cached
+    if (cachedProposalDetails) {
+      return cachedProposalDetails;
+    } else {
+      // TODO: The right typesafe handling with JoyEnum would be very useful here
+      const rawDetails = await this.rawProposalDetails(id);
+      const type = rawDetails.type as ProposalType;
+      let details: any[];
+      if (type === 'RuntimeUpgrade') {
+        // In case of RuntimeUpgrade proposal we override details to just contain the hash and filesize
+        // (instead of full WASM bytecode)
+        const wasm = rawDetails.value as Bytes;
+        details = [blake2AsHex(wasm, 256), wasm.length];
+      } else {
+        const detailsJSON = rawDetails.value.toJSON();
+        details = Array.isArray(detailsJSON) ? detailsJSON : [detailsJSON];
+      }
+      // Save entry in cache
+      this.proposalDetailsCache[id.toNumber()] = { type, details };
+
+      return { type, details };
+    }
+  }
+
   async proposalById (id: ProposalId): Promise<ParsedProposal> {
-    const rawDetails = (await this.proposalDetailsById(id)).toJSON() as { [k: string]: any };
-    const type = Object.keys(rawDetails)[0] as ProposalType;
-    const details = Array.isArray(rawDetails[type]) ? rawDetails[type] : [rawDetails[type]];
+    const { type, details } = await this.typeAndDetails(id);
     const rawProposal = await this.rawProposalById(id);
     const proposer = (await this.membersT.memberProfile(rawProposal.proposerId)).toJSON() as ParsedMember;
     const proposal = rawProposal.toJSON() as {
@@ -102,7 +139,7 @@ export default class ProposalsTransport extends BaseTransport {
   }
 
   async activeProposals () {
-    const activeProposalIds = await this.proposalsEngine.activeProposalIds<ProposalId[]>();
+    const activeProposalIds = (await this.proposalsEngine.activeProposalIds()) as Vec<ProposalId>;
 
     return Promise.all(activeProposalIds.map(id => this.proposalById(id)));
   }
@@ -112,13 +149,9 @@ export default class ProposalsTransport extends BaseTransport {
     return proposals.filter(({ proposerId }) => member.eq(proposerId));
   }
 
-  async proposalDetails (id: ProposalId) {
-    return this.proposalsCodex.proposalDetailsByProposalId(id);
-  }
-
   async voteByProposalAndMember (proposalId: ProposalId, voterId: MemberId): Promise<VoteKind | null> {
-    const vote = await this.proposalsEngine.voteExistsByProposalByVoter<VoteKind>(proposalId, voterId);
-    const hasVoted = (await this.proposalsEngine.voteExistsByProposalByVoter.size(proposalId, voterId)).toNumber();
+    const vote = (await this.proposalsEngine.voteExistsByProposalByVoter(proposalId, voterId)) as VoteKind;
+    const hasVoted = (await this.api.query.proposalsEngine.voteExistsByProposalByVoter.size(proposalId, voterId)).toNumber();
     return hasVoted ? vote : null;
   }
 
@@ -177,7 +210,7 @@ export default class ProposalsTransport extends BaseTransport {
   }
 
   async subscribeProposal (id: number|ProposalId, callback: () => void) {
-    return this.proposalsEngine.proposals(id, callback);
+    return this.api.query.proposalsEngine.proposals(id, callback);
   }
 
   async discussion (id: number|ProposalId): Promise<ParsedDiscussion | null> {

+ 4 - 3
pioneer/packages/joy-utils/src/transport/workingGroups.ts

@@ -12,18 +12,19 @@ import { OpeningId, ApplicationId, Opening, Application, ActiveOpeningStageKey }
 import { MultipleLinkedMapEntry } from '../LinkedMapEntry';
 import { Stake, StakeId } from '@joystream/types/stake';
 import { RewardRelationshipId, RewardRelationship } from '@joystream/types/recurring-rewards';
+import { APIQueryCache } from '../APIQueryCache';
 
 export default class WorkingGroupsTransport extends BaseTransport {
   private membersT: MembersTransport;
 
-  constructor (api: ApiPromise, membersTransport: MembersTransport) {
-    super(api);
+  constructor (api: ApiPromise, cacheApi: APIQueryCache, membersTransport: MembersTransport) {
+    super(api, cacheApi);
     this.membersT = membersTransport;
   }
 
   protected queryByGroup (group: WorkingGroupKey) {
     const module = apiModuleByGroup[group];
-    return this.api.query[module];
+    return this.cacheApi.query[module];
   }
 
   public async groupMemberById (group: WorkingGroupKey, workerId: number): Promise<WorkerData | null> {