@domwoe has been doing a great job here from what I’m reading. I’ll try to give some more colour on certain aspects that were maybe still unclear.
(I think) you can think of it as being called from the management canister
Not sure that’s the best analogy (mostly want to avoid any confusion that there’s some extra inter-canister call involved here). I’d say you can think of it as being triggered by the IC periodically.
But it adds up, because you need to pay every round.
It usually means you pay every round (because your heartbeat is usually called on every round) but it actually depends on the scheduling (which you have explained in a later comment).
I’m not exactly sure how this works with inter-canister calls inside heartbeat. But I guess, although you can’t “await them” in the heartbeat, the call context is created nevertheless and at some point the response is received and executed. I don’t think it would be a good idea to do inter-canister calls in every heartbeat
I’m not sure I follow what you mean by “you can’t await in the heartbeat”. You can definitely perform inter-canister calls and await them in your heartbeat. Current execution will be stopped at the await point and will be resumed when you get the response invoking the appropriate callback. It does mean that you should be careful to not send too many requests from your heartbeat if you’re still waiting on previous ones (i.e. paraphrasing your comment that it’s not a good idea to do inter-canister calls on every heartbeat) but apart from that there’s nothing special on the system level if you make calls from your heartbeat. As far as cycles consumption is concerned, again, no difference. Caller has to pay just like you described thoroughly above.
Re the remote_heartbeat: Indeed, the canister will pay for its own computation inside the remote_heartbeat(). And it doesn’t affect your main heartbeat canister.
That said, you can, if you want, include cycles in the call you send to remote_heartbeat. Then, the remote_heartbeat canister has a choice to accept some or all of those cycles (accepting means they get added to its balance) and use those cycles however it likes (any cycles not explicitly accepted would be returned to the original caller). E.g. to send more requests to other canisters, or pay for the current or future computations and so on. But this is not necessary to happen. It’s up to the application to decide if it wants to require cycles on the requests to process them.
Another thing I wondered: The documentation mentions Xnet Calls and Xnet Byte transmission. Shouldn’t this be inter-canister calls or really only cross-subnet calls? Are there additional costs for Xnet calls in comparison to inter-canister calls on the same subnet?
The fee is the same for inter-canister calls on the same or cross subnet. I think we probably picked Xnet as the more “general” term but the idea is that the fee is the same and it should be amortised to capture both same subnet and cross-subnet communication to simplify things. Also, in the fee description in the documentation you linked we do use inter-canister calls (but I guess it was still not clear enough, maybe we should fix it).
I don’t think anything changes when using the
notify API (at least for now). There were no changes to the underlying system. There should still be a response by the callee, but the response will trap immediately. I assume the cycle accounting will done although the executions traps, but would be good if someone could validate.
Re the “idle” costs, a small correction for posterity: The charge is not happening every round, we actually do it periodically on some interval. Here’s the relevant config param.