diff --git a/apps/socketoptions.hpp b/apps/socketoptions.hpp
index 12f23da2d..319499816 100644
--- a/apps/socketoptions.hpp
+++ b/apps/socketoptions.hpp
@@ -286,7 +286,8 @@ const SocketOption srt_options [] {
{ "bindtodevice", 0, SRTO_BINDTODEVICE, SocketOption::PRE, SocketOption::STRING, nullptr},
{ "retransmitalgo", 0, SRTO_RETRANSMITALGO, SocketOption::PRE, SocketOption::INT, nullptr },
{ "cryptomode", 0, SRTO_CRYPTOMODE, SocketOption::PRE, SocketOption::INT, nullptr },
- { "maxrexmitbw", 0, SRTO_MAXREXMITBW, SocketOption::POST, SocketOption::INT64, nullptr }
+ { "maxrexmitbw", 0, SRTO_MAXREXMITBW, SocketOption::POST, SocketOption::INT64, nullptr },
+ { "sendmode", 0, SRTO_SENDMODE, SocketOption::PRE, SocketOption::INT, nullptr }
};
}
diff --git a/docs/API/API-functions.md b/docs/API/API-functions.md
index 686e1610b..0eb0d110e 100644
--- a/docs/API/API-functions.md
+++ b/docs/API/API-functions.md
@@ -2061,7 +2061,8 @@ the [`SRT_MSGCTRL::msgttl`](#SRT_MSGCTRL) field.
* `inorder`: Required to be received in the order of sending. See
[`SRT_MSGCTRL::inorder`](#SRT_MSGCTRL).
* `mctrl`: An object of [`SRT_MSGCTRL`](#SRT_MSGCTRL) type that contains extra
-parameters, including `ttl` and `inorder`.
+parameters, including `ttl` and `inorder`. See details of this structure for
+additional important details concerning these parameters.
The way this function works is determined by the mode set in options, and it has
specific requirements:
@@ -2811,10 +2812,18 @@ The sender can pass the timestamp in `MSGCTRL::srctime` of the `srt_sendmsg2(..)
function together with the packet being submitted to SRT. If the `srctime` value
is not provided (the default value of 0 is set), SRT will use the internal clock
and assign packet submission time as the packet timestamp. If the sender wants to
-explicitly assign a timestamp to a certain packet this timestamp MUST be taken
-from SRT Time Access functions. The time value provided MUST equal or exceed the
-connection start time (`srt_connection_time(..)`) of the SRT socket passed
-to `srt_sendmsg2(..)`.
+explicitly specify a timestamp for a packet, this value must be monotonic against
+the time used by SRT, that is, this time value must be later (greater) than:
+
+* the value obtained by `srt_connection_time()` for the same socket
+
+* the value specified in any earlier call of `srt_sendmsg2` for the same socket
+
+otherwise the call to `srt_sendmsg2` will fail.
+
+This time value must be in the same domain as the time values used by SRT. The
+values obtained by `srt_connection_time()` and `srt_time_now()` are in this
+domain.
The current time value of the SRT internal clock can be retrieved using
the `srt_time_now()` function.
@@ -2822,44 +2831,66 @@ the `srt_time_now()` function.
There are two known cases where you might want to use `srctime`:
1. SRT passthrough (for stream gateways).
-You may wish to simply retrieve packets from an SRT source and pass them transparently
-to an SRT output (possibly re-encrypting). In that case, every packet you read should
-preserve the original value of `srctime` as obtained from [`srt_recvmsg2`](#srt_recvmsg2),
-and the original `srctime` for each packet should be then passed to [`srt_sendmsg2`](#srt_sendmsg).
-This mechanism could be used to avoid jitter resulting from varying differences between
-the time of receiving and sending the same packet.
-2. Stable timing source.
-In the case of a live streaming procedure, when spreading packets evenly into the stream,
-you might want to predefine times for every single packet to keep time intervals perfectly equal.
-Or, if you believe that your input signal delivers packets at the exact times that should be
-assigned to them, you might want to preserve these times at the SRT receiving side
-to avoid jitter that may result from varying time differences between the packet arrival
-and the moment when sending it over SRT. In such cases you might do the following:
+You may wish to simply retrieve packets from an SRT source and pass them
+transparently to an SRT output (possibly re-encrypting). In that case, every
+packet you read should preserve the original value of `srctime` as obtained
+from [`srt_recvmsg2`](#srt_recvmsg2), and the original `srctime` for each
+packet should be then passed to [`srt_sendmsg2`](#srt_sendmsg). This mechanism
+could be used to avoid jitter resulting from varying differences between the
+time of receiving and sending the same packet.
- - At the packet arrival time, grab the current time at that moment using `srt_time_now()`.
+2. Stable timing source.
- - When you want a pre-calculated packet time, use a private relative time counter
- set at the moment when the connection was made. From the moment when your first packet
- is ready, start pre-calculating packet times relative to the connection start time obtained
- from `srt_connection_time()`. Although you still have to synchronize sending times with these
- predefined times, by explicitly specifying the source time you avoid the jitter
- resulting from a lost accuracy due to waiting time and unfortunate thread scheduling.
+In the case of a live streaming procedure, when spreading packets evenly into
+the stream, you might want to predefine times for every single packet to keep
+time intervals perfectly equal. Or, if you believe that your input signal
+delivers packets at the exact times that should be assigned to them, you might
+want to preserve these times at the SRT receiving side to avoid jitter that may
+result from varying time differences between the packet arrival
+and the moment when sending it over SRT. In such cases you might do the following:
-Note that `srctime` uses an internally defined clock that is intended to be monotonic
-(the definition depends on the build flags, see below). Because of that **the application
-should not define this time basing on values obtained from the system functions for getting
-the current system time** (such as `time`, `ftime` or `gettimeofday`). To avoid problems and
-misunderstanding you should rely exclusively on time values provided by the
-`srt_time_now()` and `srt_connection_time()` functions.
+* At the packet arrival time, grab the current time at that moment using
+`srt_time_now()`.
+
+* When you want a pre-calculated packet time, use a private relative time
+counter set at the moment when the connection was made. From the moment when
+your first packet is ready, start pre-calculating packet times relative to the
+connection start time obtained from `srt_connection_time()`. Although you still
+have to synchronize sending times with these predefined times, by explicitly
+specifying the source time you avoid the jitter resulting from a lost accuracy
+due to waiting time and unfortunate thread scheduling.
+
+3. STOW mode
+
+In the STOW mode with eager sending the application is allowed to supply
+packets faster than it is legitimate for the live mode. In this case the
+packets can remain in the sender buffer until the time comes for them to
+be sent, while the timestamp still defines the rhythm in which they should
+be sent. This timestamp then defines the relative time between packets to
+be kept when then receiver application will receive them, as well as it will
+be the ultimate time to keep in the receiver buffer (to wait for recovery)
+and the ultimate time to send in the eager mode.
+
+XXX NOT IMPLEMENTED YET: the time may be supplied automatically in STOW mode
+basing on the bandwidth specified by `SRTO_INPUTBW`. Until this is implemented
+supplying the timestamps in STOW mode is obligatory.
+
+Note that `srctime` uses an internally defined clock that is intended to be
+monotonic (the definition depends on the build flags, see below). Because of
+that **the application should not define this time basing on values obtained
+from the system functions for getting the current system time** (such as
+`time`, `ftime` or `gettimeofday`). To avoid problems and misunderstanding you
+should rely exclusively on time values provided by the `srt_time_now()` and
+`srt_connection_time()` functions.
The clock used by the SRT internal clock is determined by the following build flags:
- `ENABLE_MONOTONIC` makes use of `CLOCK_MONOTONIC` with `clock_gettime` function.
- `ENABLE_STDXXX_SYNC` makes use of `std::chrono::steady_clock`.
The default is currently to use the system clock as the internal SRT clock,
-although it's highly recommended to use one of the above monotonic clocks,
-as system clock is vulnerable to time modifications during transmission.
+although it's highly recommended to use one of the above monotonic clocks, as
+system clock is vulnerable to time modifications during transmission.
### srt_time_now
diff --git a/docs/features/encryption.md b/docs/features/encryption.md
index 67a44bfbf..956163d0c 100644
--- a/docs/features/encryption.md
+++ b/docs/features/encryption.md
@@ -162,6 +162,14 @@ To avoid this, each direction uses its own SEK and Salt:
Two older versions connected together still share the same key in both directions.
+With scheduled sending, application data enters the preschedule queue while the
+initial refresh is pending. A regular packet receives its send time only after
+the encryption gate opens and flight-window capacity is available. The
+`CUDT::checkSndTimers` key-management state machine drives the refresh and retries;
+acknowledging the refreshed key wakes the multiplexer scheduler. Invalid responses
+or delayed acknowledgments of the original handshake key do not open the gate.
+Blocked sockets do not prevent other sockets or retransmissions from progressing.
+
The cryptographic usage limit of the KEK is 248 wraps (AESKW) which means virtual infinity at the expected SEK rekeying rate (90000 years to rekey 100 keys every second).
[figure1]: images/srt-encryption-1.png
diff --git a/srtcore/buffer_snd.cpp b/srtcore/buffer_snd.cpp
index ef3114d3b..97566309b 100644
--- a/srtcore/buffer_snd.cpp
+++ b/srtcore/buffer_snd.cpp
@@ -268,6 +268,52 @@ EncryptionKeySpec CSndBuffer::encryptPacket(CSndPacket& w_packet, CCryptoControl
return f;
}
+int CSndBuffer::extractPacket(int32_t seqno, CSndPacket& w_packet, time_point& w_srctime, time_point& w_nextuniquets)
+{
+ // This one is simpler - you just extract packet with given seqno.
+ // Should be unique, but just trust the seqno. Reject returning anything
+ // only if this seqno is not found in the buffer.
+ ScopedLock bufferguard(m_BufLock);
+
+ w_nextuniquets = time_point(); // default: none
+
+ // 1. Empty buffer
+ if (m_Packets.empty())
+ {
+ HLOGC(bslog.Debug, log << "extractPacket(%" << seqno << "): sender buffer empty");
+ return READ_NONE;
+ }
+
+ // XXX unused? time_point entertime = steady_clock::now();
+
+ int offset = CSeqNo::seqoff(m_iSndLastDataAck, seqno);
+ if (offset < 0 || offset >= int(m_Packets.size()))
+ {
+ HLOGC(bslog.Debug, log << "extractPacket(%" << seqno << "): not in the buffer");
+ return READ_NONE;
+ }
+
+ SndPktArray::Packet& p = m_Packets[offset];
+ w_srctime = p.m_tsOriginTime;
+
+ w_packet.pkt.m_pcData = p.m_pcData;
+ w_packet.pkt.setLength(p.m_iLength, m_iBlockLen);
+ w_packet.acquire((p), this);
+
+ w_packet.pkt.set_seqno(p.m_iSeqNo);
+ w_packet.pkt.set_msgflags(p.m_iMsgNoBitset);
+
+ HLOGC(bslog.Debug, log << CONID() << "CSndBuffer: SCHEDULED packet to send: size=" << p.m_iLength
+ << " #" << w_packet.pkt.getMsgSeq()
+ << " %" << w_packet.pkt.seqno()
+ << " !" << BufferStamp(w_packet.pkt.m_pcData, w_packet.pkt.getLength()));
+
+ if (offset + 1 < int(m_Packets.size())) // WE DO HAVE a next packet after this one
+ w_nextuniquets = m_Packets[offset+1].m_tsOriginTime;
+
+ return p.m_iLength;
+}
+
int CSndBuffer::extractUniquePacket(CSndPacket& w_packet, time_point& w_srctime, int32_t& w_lastseqno,
time_point& w_nextuniquets)
{
@@ -588,6 +634,75 @@ int CSndBuffer::popLostSeq()
return seq;
}
+// This function should return the first loss
+pair CSndBuffer::peekRexmit(const duration& min_rexmit_interval, time_point& w_send_time)
+{
+ ScopedLock bufferguard(m_BufLock);
+
+ int seq = SRT_SEQNO_NONE;
+
+ HLOGC(qslog.Debug, log << "REXMIT: looking for loss report since %" << m_iSndLastDataAck << "...");
+
+ time_point now = steady_clock::now();
+
+ pair offs = m_Packets.peekLoss(min_rexmit_interval, now, (w_send_time));
+
+ // No loss found - return 0: no lost packets extracted.
+ if (offs.first == -1)
+ {
+ HLOGC(qslog.Debug, log << "REXMIT: no loss found");
+ return make_pair(seq, seq);
+ }
+
+ seq = CSeqNo::incseq(m_iSndLastDataAck, offs.first);
+ return make_pair(seq, offs.second == -1 ? SRT_SEQNO_NONE : CSeqNo::incseq(m_iSndLastDataAck, offs.second));
+}
+
+int CSndBuffer::extractRexmitPacket(int32_t seqno, CSndPacket& w_sndpkt, sync::steady_clock::time_point& w_tsOrigin)
+{
+ ScopedLock bufferguard(m_BufLock);
+
+ int offset = CSeqNo::seqoff(m_iSndLastDataAck, seqno);
+ if (offset < 0 || offset >= int(m_Packets.size()))
+ {
+ // If a release attempt is done, it usually means that this sequence
+ // IS PRESENT in the buffer; such a situation shouldn't happen.
+ LOGC(bslog.Fatal, log << "IPE: CSndBuffer::releasePacket:%" << seqno << " with in-buffer %"
+ << m_iSndLastDataAck << "+" << m_Packets.size() << " - OUT OF RANGE REQUEST");
+ return 0;
+ }
+
+ // Ok, now we know that offset is valid; now check if it is eligible
+ // for loss retransmission; if not, return 0.
+ if (!m_Packets.extractLoss(offset))
+ {
+ return 0;
+ }
+
+ // Ok, the packet at this position was revoked from loss,
+ // return it. We don't use readPacketInternal because it hadles the
+ // stale state. In scheduled mode stale state should be handled in peekLoss().
+ SndPktArray::Packet* p = &m_Packets[offset];
+
+ w_sndpkt.pkt.m_pcData = p->m_pcData;
+ const int readlen = p->m_iLength;
+ w_sndpkt.pkt.setLength(readlen, m_iBlockLen);
+
+ // We state that the requested seqno refers to a historical (not unique)
+ // packet, hence the encryption action has encrypted the data and updated
+ // the flags.
+ w_sndpkt.pkt.set_msgflags(p->m_iMsgNoBitset);
+ w_tsOrigin = p->m_tsOriginTime;
+
+ // This function is called when packet retransmission is triggered.
+ // Therefore we are setting the rexmit time.
+ p->m_tsRexmitTime = steady_clock::now();
+
+ ++p->m_iBusy;
+ w_sndpkt.acquire_busy(p->m_iSeqNo, this);
+
+ return readlen;
+}
void CSndBuffer::releasePacket(int32_t seqno)
{
@@ -1772,6 +1887,298 @@ int SndPktArray::extractFirstLoss(const duration& miniv, const time_point& now)
return -1;
}
+explicit_t SndPktArray::extractLoss(int offset)
+{
+ // No loss at all
+ if (m_iFirstRexmit == -1)
+ {
+ SRT_ASSERT(m_iLastRexmit == -1);
+ return false;
+ }
+
+ Packet& p = m_PktQueue[offset];
+
+ // This packet is not scheduled for retransmission.
+ // (if it's zombie, simply leave it there).
+ if (sync::is_zero(p.m_tsNextRexmitTime))
+ return false;
+
+ // Clear this as retransmitted.
+ p.m_tsNextRexmitTime = time_point();
+
+ // Ok, first find it among the lists.
+ int recbegin = -1;
+ int recprev = -1;
+ int recoff = 0;
+
+ if (offset < m_iFirstRexmit)
+ {
+ // XXX not sure if this really is IPE. Could be that it disappeared in the meantime.
+ // LOGC(bslog.Error, log << "IPE: extractLoss: offset=" << offset << " < first=" << m_iFirstRexmit);
+ return false;
+ }
+
+ // Identify the record that contains this one.
+ if (offset >= m_iLastRexmit)
+ {
+ recbegin = m_iLastRexmit;
+ recoff = offset - recbegin;
+ if (recoff >= m_PktQueue[recbegin].m_iLossLength)
+ return false;
+ }
+ else
+ {
+ Tie(recbegin, recprev) = find_loss_rec(offset);
+ if (recbegin == -1)
+ {
+ // Not found even after past the last one.
+ return false;
+ }
+ recoff = offset - recbegin;
+ }
+
+ // Alright, we have then our record. Now we remove the packet.
+
+ // Now check if we want to just modify a single record, or
+ // eliminate the record entirely. The second one is only if
+ // you have position 0.
+
+ int updated_next_record = -1;
+ Packet& recp = m_PktQueue[recbegin];
+ if (recoff == 0 && recp.m_iLossLength == 1)
+ {
+ if (offset == m_iLastRexmit)
+ {
+ if (offset == m_iFirstRexmit)
+ {
+ // Doing this now because this branch does quick return.
+ recp.m_iLossLength = 0;
+ recp.m_iNextLossRecOff = 0;
+
+ // Simplest: remove eveything.
+ recp.m_iLossLength = 0;
+ recp.m_iNextLossRecOff = 0;
+ m_iFirstRexmit = -1;
+ m_iLastRexmit = -1;
+ m_iLossLengthCache = 0;
+
+ return true; // nothing more to do
+ }
+ // updated_next_record = -1; keep this value.
+ }
+ else
+ {
+ // Still could be first, but that we will know from recorev.
+ updated_next_record = recbegin + recp.m_iNextLossRecOff;
+ }
+ // Must have been delayed because previous m_iNextLossRecOff is used above.
+ recp.m_iLossLength = 0;
+ recp.m_iNextLossRecOff = 0;
+ }
+ else
+ {
+ // MINIMUM 2 packets.
+ // Check edge cases
+
+ int len_before = offset - recbegin;
+ int len_after = recp.m_iLossLength - recoff;
+
+ // These two conditions cannot be satisfied simultaneously
+ // when loss lenght == 1 is ruled out.
+ if (len_before == 0)
+ {
+ // Simple: shift record by one
+ int newrec = recbegin + 1;
+ m_PktQueue[newrec].m_iLossLength = recp.m_iLossLength - 1;
+ if (offset != m_iLastRexmit) // IS NOT last => update next ptr
+ {
+ m_PktQueue[newrec].m_iNextLossRecOff = recp.m_iNextLossRecOff - 1;
+ }
+ else
+ {
+ m_iLastRexmit = newrec;
+ }
+
+ // The cell previously starting the record is now free.
+ recp.m_iLossLength = 0;
+ recp.m_iNextLossRecOff = 0;
+
+ // Set it because it was shifted
+ updated_next_record = newrec;
+ }
+ else if (len_after == 1) // length = last + 1
+ {
+ // No change in the position, just cut the tail.
+ --recp.m_iLossLength;
+ }
+ else
+ {
+ // This means we have at least 3 packets in the record.
+ // (next record is still where it was before).
+
+ int newrec = offset + 1;
+ int nextrecoff;
+ if (recbegin == m_iLastRexmit)
+ {
+ nextrecoff = 0;
+ m_iLastRexmit = newrec;
+ }
+ else
+ {
+ nextrecoff = recbegin + recp.m_iNextLossRecOff - newrec;
+ }
+
+ m_PktQueue[newrec].m_iLossLength = len_after - 1; // except the removed one
+ m_PktQueue[newrec].m_iNextLossRecOff = nextrecoff;
+
+ recp.m_iLossLength = len_before;
+ recp.m_iNextLossRecOff = newrec - recbegin;
+ }
+ }
+
+ if (updated_next_record != -1)
+ {
+ // BUT: we also need to update the previous one.
+
+ // For last record we haven't checked previous; find it now.
+ // NOTE: DO NOT find if recprev == -1 because it means that
+ // we already know that recbegin == m_iFirstRexmit.
+ if (recbegin == m_iLastRexmit)
+ {
+ Tie(recbegin, recprev) = find_loss_rec(offset);
+ if (recbegin == -1)
+ {
+ // Not found even after past the last one.
+ // (just in case - at this stage should be impo)
+ return false;
+ }
+ }
+
+ if (recprev == -1)
+ {
+ m_iFirstRexmit = updated_next_record;
+ }
+ else
+ {
+ m_PktQueue[recprev].m_iNextLossRecOff = updated_next_record - recprev;
+ if (recbegin == m_iLastRexmit)
+ {
+ m_iLastRexmit = updated_next_record;
+ }
+ }
+ }
+
+ --m_iLossLengthCache;
+
+ string msg SRT_ATR_UNUSED;
+ SRT_ASSERT(validateLossIntegrity((msg)));
+
+ return true;
+}
+
+
+// Special version for scheduled sending: peek the next candidate
+// and the follower, if found. Still do remove all outdated loss
+// entries, skip too early entries etc.
+pair SndPktArray::peekLoss(const duration& miniv, const time_point& now, time_point& w_send_time)
+{
+ // No loss at all
+ if (m_iFirstRexmit == -1)
+ return make_pair(-1, -1);
+
+ // Exactly one packet available (we believe that "zombies"
+ // can only follow at least one remaining loss record, so it's not possible
+ // if only one loss record is available
+ if (m_iLossLengthCache == 1)
+ {
+ // We have exactly one packet; report it, regardless of conditions.
+ return make_pair(m_iFirstRexmit, -1);
+ }
+
+ // Note that records may have:
+ // - "zombie": marked for non-eligible by cleared time - remove if possible
+ // - "too new": such time that time + miniv > now() - this record must remain
+ //
+ // If during the search there was no "too new" record found yet, remove
+ // every "zombie" on the way, and also the extracted record. If any "too
+ // new" was found, do not remove anything anymore and the qualified record
+ // should be also marked "zombie" when retrieved (none found is also possible).
+ int stop_revoke = -1;
+ int last_cleared = -1;
+ bool skipped_too_new = false;
+
+ int off_planned = -1, off_next = -1;
+
+ // Try to find the first one with skipping all "too early" ones.
+ for (int loss_begin = m_iFirstRexmit; loss_begin != -1; loss_begin = next_loss_rec(loss_begin))
+ {
+ int loss_end = loss_begin + m_PktQueue[loss_begin].m_iLossLength;
+
+ for (int i = loss_begin; i != loss_end; ++i)
+ {
+ SndPktArray::Packet& p = m_PktQueue[i];
+ if (!is_zero(p.m_tsNextRexmitTime))
+ {
+ // Ok, so this cell will be taken, but it might be the future.
+ if (!p.updated_rexmit_time_passed(now, miniv))
+ {
+ if (stop_revoke == -1 && i > 0)
+ stop_revoke = i - 1;
+ skipped_too_new = true;
+ HLOGC(qslog.Debug, log << "... skipped +" << i << " - too early by "
+ << FormatDurationAuto(now + miniv - p.m_tsNextRexmitTime));
+ continue;
+ }
+
+ // --- found ---
+ off_planned = i;
+ // Check the next loss
+ off_next = i + 1;
+ if (off_next == loss_end)
+ {
+ // Worst case, it will turn back to -1.
+ off_next = next_loss_rec(off_planned);
+ }
+ goto Break2; // break 2 loops
+ }
+ else // Zombie
+ {
+ HLOGC(qslog.Debug, log << "... skipped +" << i << " - cleared earlier");
+ if (!skipped_too_new)
+ last_cleared = i;
+ }
+ // If it was cleared, continue searching.
+ }
+ }
+
+Break2:
+
+ if (last_cleared != -1)
+ {
+ remove_loss(last_cleared);
+ }
+
+ // If not found any "old enough" loss record, simply return
+ // the first and second loss you found. We know that there
+ // at least 2 loss reports.
+ if (off_planned == -1)
+ {
+ off_planned = m_iFirstRexmit;
+ if (m_PktQueue[off_planned].m_iLossLength > 1)
+ {
+ off_next = off_planned + 1;
+ }
+ else
+ {
+ off_next = next_loss_rec(m_iFirstRexmit);
+ }
+ }
+
+ w_send_time = m_PktQueue[off_planned].m_tsOriginTime;
+
+ return make_pair(off_planned, off_next);
+}
+
// Debug support
string SndPktArray::show_external(int32_t seqno, int32_t lastsent_seqno) const
{
diff --git a/srtcore/buffer_snd.h b/srtcore/buffer_snd.h
index 80c92a60e..4801d9e82 100644
--- a/srtcore/buffer_snd.h
+++ b/srtcore/buffer_snd.h
@@ -248,6 +248,13 @@ struct SndPktArray
// For spontaneous extraction (request to get the first still eligible)
int extractFirstLoss(const duration& min_interval, const time_point& now);
+ // This tries to extract the packet with explicitly given seqno.
+ explicit_t extractLoss(int32_t seqno);
+
+ // For scheduled extraction: first find out what packets are still eligible,
+ // then extract the exact ones.
+ std::pair peekLoss(const duration& miniv, const time_point& now, time_point& w_send_time);
+
// Lightweight version of extractFirstLoss for testing purposes only.
// Finds the first packet marked as loss and removes this mark.
int extractFirstLoss()
@@ -439,6 +446,16 @@ class CSndBuffer
int extractUniquePacket(CSndPacket& w_packet, time_point& w_origintime, int32_t& w_lastseqno,
time_point& w_nextuniquets);
+ /// Get access to the packet at the unique position with pointed sequence number.
+ ///
+ /// @param [out] w_packet Object to keep the packet
+ /// @param [out] w_origintime Scheduling time of the packet
+ /// @param [out] w_nextuniquets Set to the time of the packet next to extracted unique or zero time if no such packet
+ /// @param [in] seqno Sequence number of the packet to extract
+ /// @return Actual length of data read.
+ SRT_TSA_NEEDS_NONLOCKED(m_BufLock)
+ int extractPacket(int32_t seqno, CSndPacket& w_packet, time_point& w_origintime, time_point& w_nextuniquets);
+
struct DropRange
{
static const size_t BEGIN = 0, END = 1;
@@ -452,10 +469,20 @@ class CSndBuffer
SRT_TSA_NEEDS_NONLOCKED(m_BufLock)
int readOldPacket(int32_t seqno, CSndPacket& w_packet, time_point& w_origintime, DropRange& w_drop);
+ // Normal sending
+
SRT_TSA_NEEDS_NONLOCKED(m_BufLock)
int extractFirstRexmitPacket(const duration& min_rexmit_interval, int32_t& w_current_seqno, CSndPacket& w_sndpkt,
sync::steady_clock::time_point& w_tsOrigin, std::vector& w_drops);
+ // Scheduled sending
+
+ SRT_TSA_NEEDS_NONLOCKED(m_BufLock)
+ std::pair peekRexmit(const duration& min_rexmit_interval, time_point& w_send_time);
+
+ SRT_TSA_NEEDS_NONLOCKED(m_BufLock)
+ int extractRexmitPacket(int32_t seqno, CSndPacket& w_sndpkt, sync::steady_clock::time_point& w_tsOrigin);
+
private:
SRT_TSA_NEEDS_LOCKED(m_BufLock)
int readPacketInternal(int offset, CSndPacket& w_packet, time_point& w_origintime, DropRange& w_drop);
diff --git a/srtcore/congctl.cpp b/srtcore/congctl.cpp
index 054897591..41b06cdff 100644
--- a/srtcore/congctl.cpp
+++ b/srtcore/congctl.cpp
@@ -631,6 +631,19 @@ SrtCongestion::NamePtr SrtCongestion::congctls[N_CONTROLLERS] =
{"file", Creator::Create }
};
+bool SrtCongestion::select_sendmode(int sendmode)
+{
+ // 0 should be handled already, but just follow it.
+ if (sendmode > 0)
+ {
+ selector = 1; // "file" CC
+ }
+ else
+ {
+ selector = 0; // "live" (normal live mode)
+ }
+ return true;
+}
bool SrtCongestion::configure(CUDT* parent)
{
diff --git a/srtcore/congctl.h b/srtcore/congctl.h
index 2bdf7a9ed..f4d9de648 100644
--- a/srtcore/congctl.h
+++ b/srtcore/congctl.h
@@ -80,6 +80,19 @@ class SrtCongestion
return true;
}
+ bool select(const std::string& name, int sendmode)
+ {
+ // Ok, if sendmode is 0, allow selection by name.
+ // Otherwise enforce "live" mode by all means, but select
+ // the "file" CC.
+ if (sendmode == 0)
+ return select(name);
+
+ return select_sendmode(sendmode);
+ }
+
+ bool select_sendmode(int sendmode);
+
std::string selected_name()
{
if (selector == N_CONTROLLERS)
diff --git a/srtcore/core.cpp b/srtcore/core.cpp
index a0132384b..784b6b58e 100644
--- a/srtcore/core.cpp
+++ b/srtcore/core.cpp
@@ -246,6 +246,11 @@ void SocketKeeper::acquire_socket(CUDTSocket* s)
void SocketKeeper::acquire_LOCKED(CUDTSocket* s)
{
+ SRT_ASSERT(s);
+ SRT_ASSERT(!socket || socket == s);
+ if (!s || (socket && socket != s))
+ return;
+
socket = s;
s->apiAcquire();
}
@@ -1144,7 +1149,7 @@ void CUDT::open()
m_tsNextACKTime.store(currtime + m_tdACKInterval);
m_tsNextNAKTime.store(currtime + m_tdNAKInterval);
m_tsLastRspAckTime = currtime;
- m_tsLastSndTime.store(currtime);
+ m_LastSend.collect(currtime);
#if SRT_ENABLE_BONDING
m_tsUnstableSince = steady_clock::time_point();
@@ -2216,7 +2221,7 @@ CUDT::MsgHandled CUDT::processSrtMsg(const CPacket *ctrlpkt)
HLOGC(cnlog.Debug,
log << CONID() << "Dispatching message type=" << etype << " data length=" << (len / sizeof(int32_t)));
- ScopedLock cl (m_ConnectionLock);
+ UniqueLock cl (m_ConnectionLock);
if (!stillConnected())
{
HLOGC(inlog.Debug, log << CONID() << "... NOT DISPATCHING - connection is closed");
@@ -2253,8 +2258,13 @@ CUDT::MsgHandled CUDT::processSrtMsg(const CPacket *ctrlpkt)
{
const bool was_gated = m_CryptoControl.isSndDataGated();
m_CryptoControl.processSrtMsg_KMRSP(srtdata, len, m_uPeerSrtVersion, false);
- if (was_gated && !m_CryptoControl.isSndDataGated())
+ const bool became_ready = was_gated && !m_CryptoControl.isSndDataGated();
+ const bool scheduled = m_config.uSenderMode != 0;
+ if (became_ready && !scheduled)
m_pMuxer->updateSendNormal(m_parent);
+ cl.unlock();
+ if (became_ready && scheduled)
+ m_pMuxer->notifyScheduledSendReady();
return MSGH_OK; // nothing to do
}
@@ -2700,6 +2710,8 @@ bool CUDT::interpretSrtHandshake(CUDTSocket* lsn SRT_ATR_UNUSED, const CHandShak
if (IsSet(ext_flags, CHandShake::HS_EXT_HSREQ))
{
HLOGC(cnlog.Debug, log << CONID() << FUNID() << ": extracting HSREQ/RSP type extension");
+ m_CryptoControl.setPeerSecDist(IsSet(m_uPeerSrtFlags, SRT_OPT_SECDIST));
+
uint32_t *begin = p;
uint32_t *next = 0;
size_t length = size / sizeof(uint32_t);
@@ -6340,7 +6352,7 @@ SRT_REJECT_REASON CUDT::setupCC()
}
}
- bool res = m_CongCtl.select(m_config.sCongestion.str());
+ bool res = m_CongCtl.select(m_config.sCongestion.str(), m_config.uSenderMode);
if (!res || !m_CongCtl.configure(this))
{
return SRT_REJ_CONGESTION;
@@ -6391,7 +6403,7 @@ SRT_REJECT_REASON CUDT::setupCC()
m_tsNextACKTime.store(currtime + m_tdACKInterval);
m_tsNextNAKTime.store(currtime + m_tdNAKInterval);
m_tsLastRspAckTime = currtime;
- m_tsLastSndTime.store(currtime);
+ m_LastSend.collect(currtime);
#ifdef SRT_ENABLE_RATE_MEASUREMENT
HLOGC(bslog.Debug, log << CONID() << "RATE-MEASUREMENT: initializing time TS=" << FormatTime(currtime));
@@ -7181,12 +7193,28 @@ int CUDT::sendmsg2(const char *data, int len, SRT_MSGCTRL& w_mctrl)
throw CUDTException(MJ_NOTSUP, MN_INVALMSGAPI);
}
+ /* XXX Check again. When using file mode at the reception side, the
+ timestamp is ignored anyway, so this is likely not necessary.
if ((!m_config.bMessageAPI || !m_bTsbPd))
{
HLOGC(aslog.Warn, log << CONID()
<< "Source time can only be used with TSBPD and Message API enabled. Using default time instead.");
w_mctrl.srctime = 0;
}
+ */
+ }
+ else
+ {
+ // SRCTIME is undefined - this is not acceptable in STOW sender modes
+ // XXX NOTE: it should be doable to tolerate this situation if SRT
+ // can simply generate the right source times, or when there's some
+ // method to allow extended time for recovery at the receiver side.
+ if (m_config.uSenderMode != 0)
+ {
+ LOGC(aslog.Error, log << CONID() << "sendmsg2: with sender mode=" << m_config.uSenderMode
+ << " declaring packet send time is obligatory");
+ throw CUDTException(MJ_NOTSUP, MN_INVALMSGAPI);
+ }
}
const int64_t send_srctime = w_mctrl.srctime
@@ -7225,9 +7253,16 @@ int CUDT::sendmsg2(const char *data, int len, SRT_MSGCTRL& w_mctrl)
}
}
- // Insert this socket to the snd list if it is not on the list already.
- // m_pMuxer->sndUList()->pop may lock CSndUList::m_ListLock and then m_RecvAckLock
- m_pMuxer->updateSendNormal(m_parent);
+ if (m_config.uSenderMode == 0)
+ {
+ // Insert this socket to the snd list if it is not on the list already.
+ m_pMuxer->updateSendNormal(m_parent);
+ }
+ else
+ {
+ time_point dst_time = time_point() + microseconds_from(w_mctrl.srctime);
+ scheduleRegular(seqno, steady_clock::now(), dst_time);
+ }
#ifdef SRT_ENABLE_ECN
// IF there was a packet drop on the sender side, report congestion to the app.
@@ -7242,6 +7277,143 @@ int CUDT::sendmsg2(const char *data, int len, SRT_MSGCTRL& w_mctrl)
return size;
}
+// [[affinity(main)]]
+void CUDT::scheduleRegular(int32_t seqno, const time_point& currtime, const time_point& exp_sendtime)
+{
+ m_LastSched.lazyStart(seqno, currtime);
+ time_point delivery = exp_sendtime + milliseconds_from(m_iPeerTsbPdDelay_ms) + slippedRTT(2);
+ m_pMuxer->scheduleSend(m_parent, seqno, sched::TP_REGULAR, exp_sendtime, delivery);
+}
+
+void CUDT::scheduleRexmit(int32_t seqno, const time_point& exp_sendtime)
+{
+ time_point delivery = exp_sendtime + milliseconds_from(m_iPeerTsbPdDelay_ms) + slippedRTT(2);
+ m_pMuxer->scheduleSend(m_parent, seqno, sched::TP_REXMIT, exp_sendtime, delivery);
+}
+
+
+steady_clock::time_point CSendPipeManager::updateNext(const steady_clock::time_point& currtime,
+ const steady_clock::duration& send_interval)
+{
+ duration passed_time = currtime - m_tsTime.load();
+
+ int deviation = count_microseconds(send_interval - passed_time);
+ m_tdAverageDeviation_us = avg_iir_lazy<16>(m_tdAverageDeviation_us, deviation);
+
+ time_point nextperiod_time = m_tsTime.load() + send_interval;
+ time_point forfeit_time = currtime - send_interval * 2;
+
+ HLOGC(qslog.Debug, log << "PLAN: now=" << FormatTime(currtime)
+ << " LAST:" << FormatDurationAuto(m_tsTime.load() - currtime, true)
+ << " NEXT:" << FormatDurationAuto(nextperiod_time - currtime, true)
+ << " PAST-LAST: " << FormatDurationAuto(forfeit_time - nextperiod_time, true));
+
+ if (nextperiod_time + seconds_from(1) < currtime)
+ nextperiod_time = forfeit_time;
+
+ if (nextperiod_time < currtime)
+ {
+ nextperiod_time = currtime;
+ }
+ // Remember this as the last sending time
+ // NOTE: We remember this as the last sending time,
+ // regardless if we have used the time override or not.
+ // Time override is used when a regular packet's expected sending
+ // time is earlier than nextperiod_time. In this case we send it
+ // faster than according to the current speed, but we still remember
+ // this time as the last sent in order to keep up with the speed.
+ m_tsTime = nextperiod_time;
+
+ return m_tsTime;
+}
+
+// [[using affinity(CSndQueue::m_WorkerThread)]]
+// (Can't use SRT_ASSERT_AFFINITY because CUDT has no access to CMultiplexer::m_SndQueue)
+bool CUDT::planSendingTime(sched::Type type, const CUDT::time_point& latest_delivery, CUDT::time_point& w_sendtime)
+{
+ // type: check if TP_REXMIT, otherwise it's regular
+ // latest_delivery: the latest possible time when it should be delivered
+ // w_sendtime:
+ // [IN] The PLANNED time for the packet (in eager mode, the latest time to send the regular packet)
+ // [ONT] The resulting time when the packet should be sent
+ //
+ // Return:
+ // * true: this scheduled packet shall be sent
+ // * false: please drop this send request
+
+ // Ok, the state update rules:
+ //
+ // m_tsTime is the time when the packet was sent last time.
+ // This should be set to the time written into w_sendtime.
+ // All calculations start from this time. It should not be zero any time.
+
+ time_point currtime = steady_clock::now();
+
+ // Could assert, but let's just do a quick check
+ if (m_LastSched.updateFirst(currtime))
+ {
+ HLOGC(qslog.Debug, log << "PLAN: First time, set as NOW: " << FormatTime(m_LastSched.lastTime()));
+
+ // Yes, will be in the past during pickup; doesn't matter.
+ w_sendtime = m_LastSched.lastTime();
+ if (type == sched::TP_REXMIT && w_sendtime > latest_delivery)
+ return false;
+ return true;
+ }
+
+ time_point nextperiod_time = m_LastSched.updateNext(currtime, m_tdSendInterval);
+
+ // Ok, so nextperiod_time is now the time that should be set
+ // as w_sendtime; we need to check it if it's in forward.
+ // If so, take it as a good deal.
+ if (nextperiod_time <= w_sendtime)
+ {
+ HLOGC(qslog.Debug, log << "PLAN: ... PLANTIME=" << FormatTime(w_sendtime) << " - speedup by "
+ << FormatDurationAuto(w_sendtime - nextperiod_time));
+
+ if (type == sched::TP_REXMIT && nextperiod_time > latest_delivery)
+ return false;
+ w_sendtime = nextperiod_time;
+ return true;
+ }
+
+ // Ok, now we are past the intended sending time.
+ // Additional rules:
+
+ // For regular (first-time, unique) packets, we allow
+ // for overdrive, no matter how fast it would get.
+
+ if (type != sched::TP_REXMIT)
+ {
+ HLOGC(qslog.Debug, log << "PLAN: ... PLANTIME=" << FormatTime(w_sendtime) << " KEPT - override NEXTSEND by "
+ << FormatDurationAuto(nextperiod_time - w_sendtime));
+ // REGULAR
+ // Leave the send time as is, while keeping the last
+ // sending time as the alleged next sending time.
+ return true;
+ }
+
+ // For retransmitted packets, qualify them to send at
+ // this predicted time. Except they are already past the
+ // latest possible delivery time, in which case deny them.
+
+ if (nextperiod_time > latest_delivery)
+ {
+ HLOGC(qslog.Debug, log << "PLAN:... REXMIT nextsend=" << FormatTime(nextperiod_time)
+ << " exceeds latest delivery by " << FormatDurationAuto(nextperiod_time - latest_delivery)
+ << " - NOT PLANNING");
+ return false;
+ }
+
+ // Condition: next delivery time is past w_sendtime, but still before latest_delivery
+ // Plan regularly, without override.
+
+ HLOGC(qslog.Debug, log << "PLAN:... REXMIT nextsend=" << FormatTime(nextperiod_time)
+ << " - PLANNING at this time; " << FormatDurationAuto(currtime - nextperiod_time) << " towards NOW");
+ w_sendtime = nextperiod_time;
+ return true;
+}
+
bool CUDT::updateCryptoOnSending(bool force_refresh)
{
int keyindex[2] = {-1, -1};
@@ -7663,6 +7835,12 @@ int64_t CUDT::sendfile(fstream &ifs, int64_t &offset, int64_t size, int block)
throw CUDTException(MJ_SETUP, MN_SECURITY, 0);
}
+ if (m_config.uSenderMode != 0)
+ {
+ LOGC(aslog.Error, log << CONID() << "In FILE mode the scheduled sender mode is not supported");
+ throw CUDTException(MJ_NOTSUP, MN_INVALBUFFERAPI);
+ }
+
ScopedLock sendguard (m_SendLock);
if (m_pSndBuffer->getCurrBufSize() == 0)
@@ -8466,7 +8644,7 @@ void CUDT::sendCtrl(UDTMessageType pkttype, const int32_t* lparam, void* rparam,
// Fix keepalive
if (nbsent)
- m_tsLastSndTime.store(steady_clock::now());
+ m_LastSend.update(steady_clock::now(), nbsent);
}
bool CUDT::getFirstNoncontSequence(int32_t& w_seq, string& w_log_reason)
@@ -8857,7 +9035,9 @@ bool CUDT::revokeACKedSequences(int32_t ackdata_seqno, int32_t& w_last_sent_seqn
HLOGC(inlog.Debug, log << "ACK: kicking the send schedule/cond");
// insert this socket to snd list if it is not on the list yet
- const steady_clock::time_point currtime = m_pMuxer->updateSendNormal(m_parent);
+ const steady_clock::time_point currtime = (m_config.uSenderMode == 0)
+ ? m_pMuxer->updateSendNormal(m_parent)
+ : steady_clock::now();
if (m_config.bSynSending)
{
@@ -8894,6 +9074,7 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point
// Note: minimum of one 4-byte field is granted before the call.
const int32_t ackdata_seqno = ackdata[ACKD_RCVLASTACK];
+ bool notify_scheduled_capacity = false;
// Check the value of ACK in case when it was some rogue peer
if (ackdata_seqno < 0)
@@ -8933,15 +9114,23 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point
// Process a lite ACK
if (isLiteAck)
{
- ScopedLock ack_lock(m_RecvAckLock);
- if (CSeqNo::seqcmp(ackdata_seqno, m_iSndLastAck) >= 0)
{
- m_iFlowWindowSize = m_iFlowWindowSize - CSeqNo::seqoff(m_iSndLastAck, ackdata_seqno);
- m_iSndLastAck = ackdata_seqno;
- m_iSndMinFlightSpan = getFlightSpan();
- m_tsLastRspAckTime = currtime;
- m_iReXmitCount = 1; // Reset re-transmit count since last ACK
+ ScopedLock ack_lock(m_RecvAckLock);
+ if (CSeqNo::seqcmp(ackdata_seqno, m_iSndLastAck) >= 0)
+ {
+ const int cwnd1 = std::min(m_iFlowWindowSize, m_iCongestionWindow);
+ const bool was_stuck = cwnd1 <= getFlightSpan();
+ m_iFlowWindowSize = m_iFlowWindowSize - CSeqNo::seqoff(m_iSndLastAck, ackdata_seqno);
+ m_iSndLastAck = ackdata_seqno;
+ m_iSndMinFlightSpan = getFlightSpan();
+ m_tsLastRspAckTime = currtime;
+ m_iReXmitCount = 1; // Reset re-transmit count since last ACK
+ const int cwnd = std::min(m_iFlowWindowSize, m_iCongestionWindow);
+ notify_scheduled_capacity = m_config.uSenderMode != 0 && was_stuck && cwnd > getFlightSpan();
+ }
}
+ if (notify_scheduled_capacity)
+ m_pMuxer->notifyScheduledSendReady();
return true;
}
@@ -9011,13 +9200,20 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point
m_tsLastRspAckTime = currtime;
m_iReXmitCount = 1; // Reset re-transmit count since last ACK
- const int cwnd = std::min(m_iFlowWindowSize, m_iCongestionWindow);
- if (bWasStuck && cwnd > getFlightSpan())
+ if (m_config.uSenderMode == 0)
{
- m_pMuxer->updateSendNormal(m_parent);
- HLOGC(gglog.Debug,
- log << CONID() << "processCtrlAck: could reschedule SND. iFlowWindowSize " << m_iFlowWindowSize
- << " SPAN " << getFlightSpan() << " ackdataseqno %" << ackdata_seqno);
+ const int cwnd = std::min(m_iFlowWindowSize, m_iCongestionWindow);
+ if (bWasStuck && cwnd > getFlightSpan())
+ {
+ m_pMuxer->updateSendNormal(m_parent);
+ HLOGC(gglog.Debug,
+ log << CONID() << "processCtrlAck: could reschedule SND. iFlowWindowSize " << m_iFlowWindowSize
+ << " SPAN " << getFlightSpan() << " ackdataseqno %" << ackdata_seqno);
+ }
+ }
+ else
+ {
+ notify_scheduled_capacity = bWasStuck;
}
}
@@ -9035,6 +9231,9 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point
if (CSeqNo::seqoff(m_iSndLastFullAck, ackdata_seqno) <= 0)
{
// discard it if it is a repeated ACK
+ ack_lock.unlock();
+ if (notify_scheduled_capacity && hasSendCapacity())
+ m_pMuxer->notifyScheduledSendReady();
return true;
}
m_iSndLastFullAck = ackdata_seqno;
@@ -9145,6 +9344,9 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point
updateCC(TEV_ACK, EventVariant(ackdata_seqno));
+ if (notify_scheduled_capacity && hasSendCapacity())
+ m_pMuxer->notifyScheduledSendReady();
+
m_StatsLock.lock();
m_stats.sndr.recvdAck.count(1);
m_StatsLock.unlock();
@@ -9265,6 +9467,11 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt)
// when logging is forcefully off.
int32_t wrong_loss SRT_ATR_UNUSED = SRT_SEQNO_NONE;
+ // Will be used to determine rexmit packets to schedule.
+ // If remain with this value, there's nothing to schedule.
+ int32_t first_loss = SRT_SEQNO_NONE;
+ time_point first_send_time;
+
{
#if SRT_ENABLE_BONDING
// Keep the group from disappearing in the meantime
@@ -9332,7 +9539,8 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt)
{
HLOGC(inlog.Debug, log << CONID() << "LOSSREPORT: adding "
<< losslist_lo << " - " << losslist_hi << " to loss list");
- num = m_pSndBuffer->insertLoss(losslist_lo, losslist_hi, steady_clock::now());
+ num = m_pSndBuffer->insertLoss(losslist_lo, losslist_hi, steady_clock::now(), (first_send_time));
+ first_loss = losslist_lo;
}
// ELSE losslist_lo <% m_iSndLastAck
else
@@ -9356,9 +9564,11 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt)
{
HLOGC(inlog.Debug, log << CONID() << "LOSSREPORT: adding "
<< m_iSndLastAck << "[ACK] - " << losslist_hi << " to loss list");
- num = m_pSndBuffer->insertLoss(m_iSndLastAck, losslist_hi, steady_clock::now());
+ num = m_pSndBuffer->insertLoss(m_iSndLastAck, losslist_hi, steady_clock::now(), (first_send_time));
dropreq_hi = CSeqNo::decseq(m_iSndLastAck);
IF_HEAVY_LOGGING(drop_type = "partially");
+
+ first_loss = m_iSndLastAck;
}
// In distinction to losslist, DROPREQ has always just one range,
@@ -9398,7 +9608,8 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt)
HLOGC(inlog.Debug,
log << CONID() << "LOSSREPORT: adding %" << losslist[i] << " (1 packet) to loss list");
- num = m_pSndBuffer->insertLoss(losslist[i], losslist[i], steady_clock::now());
+ num = m_pSndBuffer->insertLoss(losslist[i], losslist[i], steady_clock::now(), (first_send_time));
+ first_loss = losslist[i];
m_StatsLock.lock();
m_stats.sndr.lost.count(num);
@@ -9420,6 +9631,18 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt)
}
}
+ if (m_config.uSenderMode != 0 && first_loss != SRT_SEQNO_NONE)
+ {
+ if (!is_zero(first_send_time)) // theoretically can happen in buffer empty - paranoid check
+ {
+ scheduleRexmit(first_loss, first_send_time);
+ }
+ else
+ {
+ LOGC(qslog.Error, log << "LOSSREPORT: IPE: loss scheduled, but time is empty!");
+ }
+ }
+
if (!secure)
{
LOGC(inlog.Warn,
@@ -9448,7 +9671,8 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt)
// blind rexmit mode is laterexmit (the sender will repeat sending
// unacknowledged packets only when it has reached the limit with
// nothing more to withdraw from the sender buffer).
- m_pMuxer->updateSendNormal(m_parent);
+ if (m_config.uSenderMode == 0)
+ m_pMuxer->updateSendNormal(m_parent);
m_StatsLock.lock();
m_stats.sndr.recvdNak.count(1);
@@ -9564,7 +9788,7 @@ bool CUDT::processCtrlHS(const CPacket& ctrlpkt)
const int nbsent = channel()->sendto(m_PeerAddr, rsppkt, m_SourceAddr);
if (nbsent)
{
- m_tsLastSndTime.store(steady_clock::now());
+ m_LastSend.update(steady_clock::now(), nbsent);
}
}
@@ -10059,6 +10283,49 @@ void CUDT::retransmissionConsumeLength(size_t payload_size)
#endif
}
+int CUDT::extractPlannedLoss(SendTaskProto& proto)
+{
+ // Ok, first extract the first and next loss. NOTE:
+ // 1. It doesn't mean that any loss is eligible for sending.
+ // 2. Having both seqno set only means that there's more than one.
+
+ // All we can do here is to plan sending for the losses that are reported.
+
+ duration delivery_slip = milliseconds_from(m_iPeerTsbPdDelay_ms) + slippedRTT(2);
+
+ time_point sendtime;
+ pair nextloss = m_pSndBuffer->peekRexmit(minRexmitInterval(), (sendtime));
+
+ // RETURN:
+ // 0 - There are no lost packets scheduled, including the current one.
+ // 1 - The current packet was filled, but there are no more losses.
+ // 2 - The current was filled, and next, too.
+ if (nextloss.first == SRT_SEQNO_NONE)
+ {
+ // LOG: no loss available
+ return 0;
+ }
+
+ // Fill the first one
+ proto.m_Packet.set_socket(m_parent);
+ proto.m_Packet.m_iSeqNo = nextloss.first;
+ proto.m_Packet.m_Type = sched::TP_REXMIT;
+ proto.m_tsSendTime = sendtime;
+ proto.m_tsLatestDeliveryTime = sendtime + delivery_slip;
+
+ if (!planSendingTime(sched::TP_REXMIT, proto.m_tsLatestDeliveryTime, (proto.m_tsSendTime)))
+ {
+ HLOGC(qslog.Debug, log << "REXMIT: dropping expired request for %" << proto.m_Packet.seqno());
+ m_pSndBuffer->cancelLostSeq(proto.m_Packet.seqno());
+ proto.m_Packet = SchedPacket();
+ }
+
+ if (nextloss.second == SRT_SEQNO_NONE)
+ return 1;
+
+ return 2;
+}
+
int CUDT::packLostData(CSndPacket& w_sndpkt)
{
#ifdef SRT_ENABLE_MAXREXMITBW
@@ -10151,6 +10418,64 @@ int CUDT::packLostData(CSndPacket& w_sndpkt)
return w_packet.getLength();
}
+int CUDT::packLostData(int32_t seqno, CSndPacket& w_sndpkt)
+{
+ // Original sending time of the packet, to be stamped after filling.
+ steady_clock::time_point tsOrigin;
+
+ {
+ // protect m_iSndLastDataAck from updating by ACK processing
+ ScopedLock ackguard(m_RecvAckLock);
+
+ const int payload = m_pSndBuffer->extractRexmitPacket(seqno, (w_sndpkt), (tsOrigin));
+ m_iSndCurrSeqNo = CSeqNo::maxseq(m_iSndCurrSeqNo, seqno);
+
+ if (payload <= 0)
+ return 0;
+ }
+
+ CPacket& w_packet = w_sndpkt.pkt;
+
+ HLOGC(qslog.Debug, log << CONID() << "packed REXMIT packet %" << w_packet.seqno() << " size=" << w_packet.getLength()
+ << " - still " << m_pSndBuffer->getLossLength() << " LOSS ENTRIES left");
+
+ // POST-EXTRACTION length fixes.
+
+ // The packet has been ecrypted, thus the authentication tag is expected to be stored
+ // in the SND buffer as well right after the payload.
+ if (m_CryptoControl.getCryptoMode() == CSrtConfig::CIPHER_MODE_AES_GCM)
+ {
+ w_packet.setLength(w_packet.getLength() + HAICRYPT_AUTHTAG_MAX);
+ }
+
+#ifdef SRT_ENABLE_MAXREXMITBW
+ retransmissionConsumeLength(w_packet.getLength());
+#endif
+
+ m_StatsLock.lock();
+ m_stats.sndr.sentRetrans.count(w_packet.getLength());
+ m_StatsLock.unlock();
+
+ // Despite the contextual interpretation of packet.m_iMsgNo around
+ // CSndBuffer::readOldPacket (extractUniquePacket doesn't return -1), in this particular
+ // case we can be sure that this is exactly the value of PH_MSGNO as a bitset.
+ // So, set here the rexmit flag if the peer understands it.
+ if (m_bPeerRexmitFlag)
+ {
+ w_packet.set_msgflags(w_packet.msgflags() | PACKET_SND_REXMIT);
+ }
+ setDataPacketTS(w_packet, tsOrigin);
+
+#ifdef SRT_ENABLE_RATE_MEASUREMENT
+ m_SndRexmitMeasurement.dataUpdate(1, w_packet.getLength());
+ HLOGC(bslog.Debug, log << "RateMeasurement: REXMIT, pkt-size=" << w_packet.getLength()
+ << " - COLLECTED pkts=" << m_SndRexmitMeasurement.m_nInstaPackets
+ << " bytes=" << m_SndRexmitMeasurement.m_nInstaBytes);
+#endif
+
+ return w_packet.getLength();
+}
+
#if SRT_DEBUG_TRACE_SND
class snd_logger
{
@@ -10446,7 +10771,7 @@ bool CUDT::packData(CSndPacket& w_sndpkt, steady_clock::time_point& w_nexttime,
}
else
{
- if (!packUniqueData((w_sndpkt)))
+ if (packUniqueData((w_sndpkt)) != PACK_OK)
{
m_tsNextSendTime = steady_clock::time_point();
m_tdSendTimeDiff = steady_clock::duration::zero();
@@ -10489,7 +10814,7 @@ bool CUDT::packData(CSndPacket& w_sndpkt, steady_clock::time_point& w_nexttime,
#endif
// Fix keepalive
- m_tsLastSndTime.store(enter_time);
+ m_LastSend.update(enter_time, payload);
considerLegacySrtHandshake(steady_clock::time_point());
@@ -10557,17 +10882,128 @@ bool CUDT::packData(CSndPacket& w_sndpkt, steady_clock::time_point& w_nexttime,
return payload >= 0; // XXX shouldn't be > 0 ? == 0 is only when buffer range exceeded.
}
-bool CUDT::packUniqueData(CSndPacket& w_sndpkt)
+// Second version of packData: pack the exact data as specified in the specification
+// reported from the schedule.
+CUDT::PackResult CUDT::packData(const SchedPacket& spec, CSndPacket& w_sndpkt, CNetworkInterface& w_src_addr)
+{
+ int payload = 0;
+ bool new_packet_packed = false;
+
+ ScopedLock connectguard(m_ConnectionLock);
+ // If a closing action is done simultaneously, then
+ // m_bOpened should already be false, and it's set
+ // just before releasing this lock.
+ //
+ // If this lock is caught BEFORE the closing could
+ // start the dissolving process, this process will
+ // not be started until this function is finished.
+ if (!m_bOpened)
+ return PACK_UNAVAILABLE;
+
+ time_point enter_time = steady_clock::now();
+ w_src_addr = m_SourceAddr;
+
+ CPacket& w_packet = w_sndpkt.pkt;
+
+ IF_HEAVY_LOGGING(const char* reason = ""); // The source of the data packet (normal/rexmit/filter)
+ if (spec.type() == sched::TP_REXMIT)
+ {
+ payload = packLostData(spec.seqno(), (w_sndpkt));
+ if (payload <= 0)
+ return PACK_UNAVAILABLE;
+ IF_HEAVY_LOGGING(reason = "reXmit");
+ }
+ else if (spec.type() == sched::TP_CONTROL)
+ {
+ if (m_PacketFilter.packControlPacket(spec.seqno(), m_CryptoControl.getSndCryptoFlags(), (w_sndpkt.pkt)))
+ {
+ HLOGC(qslog.Debug, log << CONID() << "filter: filter/CTL packet ready - packing instead of data.");
+ payload = (int) w_sndpkt.pkt.getLength();
+ IF_HEAVY_LOGGING(reason = "filter");
+
+ // Stats
+ ScopedLock lg(m_StatsLock);
+ m_stats.sndr.sentFilterExtra.count(1);
+ }
+ else
+ {
+ LOGC(qslog.Error, log << CONID() << "filter: IPE: didn't provide control packet for %" << m_iSndCurrSeqNo
+ << " that has been scheduled");
+ return PACK_ERROR;
+ }
+ }
+ else // type() == TP_REGULAR
+ {
+ const PackResult result = packUniqueData(spec.seqno(), (w_sndpkt));
+ if (result != PACK_OK)
+ return result;
+#if SRT_ENABLE_MAXREXMITBW
+ if (m_zSndAveragePacketSize > 0)
+ {
+ m_zSndAveragePacketSize = avg_iir<16>(m_zSndAveragePacketSize, w_packet.getLength());
+ }
+ else
+ {
+ m_zSndAveragePacketSize = w_packet.getLength();
+ }
+ m_zSndMaxPacketSize = std::max(m_zSndMaxPacketSize, w_packet.getLength());
+#endif
+ new_packet_packed = true;
+
+ payload = (int) w_packet.getLength();
+ IF_HEAVY_LOGGING(reason = "normal");
+ }
+
+ w_packet.set_id(m_PeerID); // Set the destination SRT socket ID.
+
+ // XXX This should be done when scheduling the packet first time.
+ // The problem: m_PacketFilter has assigned affinity to the receiver worker thread,
+ // while scheduling a packet is happening in the application thread.
+ if (new_packet_packed && m_PacketFilter)
+ {
+ HLOGC(qslog.Debug, log << CONID() << "filter: Feeding packet for source clip");
+ m_PacketFilter.feedSource((w_packet));
+ }
+
+ HLOGC(qslog.Debug,
+ log << CONID() << "packData: " << reason << " packet seq=" << w_packet.seqno() << " (ACK=" << m_iSndLastAck
+ << " ACKDATA=" << m_pSndBuffer->firstSeqNo() << " MSG/FLAGS: " << w_packet.MessageFlagStr() << ")");
+
+ // This means that the packet will really be sent.
+ if (payload >= 0)
+ {
+ // Fix keepalive
+ m_LastSend.update(enter_time, payload);
+
+ considerLegacySrtHandshake(steady_clock::time_point());
+
+ // WARNING: TEV_SEND is the only event that is reported from
+ // the CSndQueue::worker thread. All others are reported from
+ // CRcvQueue::worker. If you connect to this signal, make sure
+ // that you are aware of prospective simultaneous access.
+ updateCC(TEV_SEND, EventVariant(&w_packet));
+
+ m_StatsLock.lock();
+ m_stats.sndr.sent.count(payload);
+ if (new_packet_packed)
+ m_stats.sndr.sentUnique.count(payload);
+ m_StatsLock.unlock();
+ return PACK_OK;
+ }
+
+ return PACK_ERROR;
+}
+
+CUDT::PackResult CUDT::packUniqueData(CSndPacket& w_sndpkt)
{
if (m_CryptoControl.isSndDataGated())
{
HLOGC(qslog.Debug, log << CONID() << "packUniqueData: waiting for the peer to acknowledge the independent TX key");
- return false;
+ return PACK_CONGESTED;
}
int current_sequence_number; // reflexing variable
time_point tsOrigin;
- int pld_size;
{
ScopedLock lkrack (m_RecvAckLock);
@@ -10579,14 +11015,14 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt)
HLOGC(qslog.Debug,
log << CONID() << "packUniqueData: CONGESTED: cwnd=min(" << m_iFlowWindowSize << "," << m_iCongestionWindow
<< ")=" << cwnd << " seqlen=(" << m_iSndLastAck << "-" << m_iSndCurrSeqNo << ")=" << flightspan);
- return false;
+ return PACK_CONGESTED;
}
+ IF_HEAVY_LOGGING(int32_t prev = m_iSndCurrSeqNo);
+
current_sequence_number = m_iSndCurrSeqNo; // PROXY for atomic; ALSO needed later.
time_point next_unique_ts;
- pld_size = m_pSndBuffer->extractUniquePacket((w_sndpkt), (tsOrigin), (current_sequence_number),
- (next_unique_ts));
- IF_HEAVY_LOGGING(int32_t prev = m_iSndCurrSeqNo);
+ const int pld_size = m_pSndBuffer->extractUniquePacket((w_sndpkt), (tsOrigin), (current_sequence_number), (next_unique_ts));
m_iSndCurrSeqNo = current_sequence_number;
m_tsSndNextUnique = next_unique_ts;
@@ -10595,14 +11031,48 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt)
{
HLOGC(qslog.Debug, log << "packUniqueData: nothing extracted from the buffer; skipped "
<< CSeqNo::seqoff(prev, current_sequence_number));
- return false;
+ return PACK_UNAVAILABLE;
+ }
+
+ HLOGC(qslog.Debug, log << "packUniqueData: extracted %" << current_sequence_number
+ << " after previous %" << m_iSndCurrSeqNo.load() << " (diff: "
+ << CSeqNo::seqoff(m_iSndCurrSeqNo, current_sequence_number) << ")");
+ }
+
+ return postExtractUniqueData(current_sequence_number, tsOrigin, w_sndpkt);
+}
+
+CUDT::PackResult CUDT::packUniqueData(int32_t seqno, CSndPacket& w_sndpkt)
+{
+ int current_sequence_number;
+ time_point tsOrigin;
+
+ {
+ ScopedLock lkrack (m_RecvAckLock);
+ current_sequence_number = m_iSndCurrSeqNo;
+ time_point next_unique_ts;
+ const int pld_size = m_pSndBuffer->extractPacket(seqno, (w_sndpkt), (tsOrigin), (next_unique_ts));
+ m_tsSndNextUnique = next_unique_ts;
+
+ if (pld_size == 0)
+ {
+ HLOGC(qslog.Debug, log << "packUniqueData: nothing extracted from the buffer; skipped "
+ << CSeqNo::seqoff(m_iSndCurrSeqNo, seqno));
+ return PACK_UNAVAILABLE;
}
+ m_iSndCurrSeqNo = CSeqNo::maxseq(m_iSndCurrSeqNo, seqno);
+
HLOGC(qslog.Debug, log << "packUniqueData: extracted %" << current_sequence_number
<< " after previous %" << m_iSndCurrSeqNo.load() << " (diff: "
<< CSeqNo::seqoff(m_iSndCurrSeqNo, current_sequence_number) << ")");
}
+ return postExtractUniqueData(current_sequence_number, tsOrigin, w_sndpkt);
+}
+
+CUDT::PackResult CUDT::postExtractUniqueData(int current_sequence_number, const time_point& tsOrigin, CSndPacket& w_sndpkt)
+{
CPacket& w_packet = w_sndpkt.pkt;
#if SRT_ENABLE_BONDING
@@ -10665,7 +11135,7 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt)
<< " is behind of EXTRACTION sequence " << current_sequence_number << ", dropping this packet: DIFF="
<< packetspan << " STAMP=" << BufferStamp(w_packet.m_pcData, w_packet.getLength()));
// XXX: Probably also change the socket state to broken?
- return false;
+ return PACK_ERROR;
}
int32_t upd SRT_ATR_UNUSED = gk.group->updateSentSeq(m_iSndCurrSeqNo);
@@ -10693,7 +11163,7 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt)
{
LOGC(qslog.Error, log << CONID() << "packUniqueData: encryption failed; data packet not sent");
w_sndpkt.release();
- return false;
+ return PACK_ERROR;
}
if (keyflag != EK_NOENC)
updateCryptoOnSending();
@@ -10709,7 +11179,7 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt)
m_SndRegularMeasurement.dataUpdate(1, w_packet.getLength());
#endif
- return true;
+ return PACK_OK;
}
// This is a close request, but called from the handler of the
@@ -12672,9 +13142,9 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime)
const bool is_laterexmit = m_CongCtl->rexmitMethod() == SrtCongestion::SRM_LATEREXMIT; // FileCC
const bool is_fastrexmit = m_CongCtl->rexmitMethod() == SrtCongestion::SRM_FASTREXMIT; // LiveCC
- // If the receiver will send periodic NAK reports, then FASTREXMIT (live) is inactive.
+ // FASTREXMIT (live) is inactive if the peer reports losses periodically or drops late packets.
// TODO: Probably some method of "blind rexmit" MUST BE DONE, when TLPKTDROP is off.
- if (is_fastrexmit && m_bPeerNakReport)
+ if (is_fastrexmit && (m_bPeerNakReport || m_bPeerTLPktDrop))
return;
if ((!is_laterexmit || m_pSndBuffer->getLossLength() == 0))
@@ -12683,7 +13153,8 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime)
if (getFlightSpan() > 0)
{
const int32_t csn = m_iSndCurrSeqNo;
- const int num = m_pSndBuffer->insertLoss(m_iSndLastAck, csn, steady_clock::now());
+ time_point first_send_time;
+ const int num = m_pSndBuffer->insertLoss(m_iSndLastAck, csn, steady_clock::now(), (first_send_time));
if (num > 0)
{
m_StatsLock.lock();
@@ -12694,6 +13165,11 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime)
log << CONID() << "ENFORCED " << (is_laterexmit ? "LATEREXMIT" : "FASTREXMIT")
<< " by ACK-TMOUT (scheduling): " << CSeqNo::incseq(m_iSndLastAck) << "-" << csn << " ("
<< CSeqNo::seqoff(m_iSndLastAck, csn) << " packets)");
+
+ if (m_config.uSenderMode != 0)
+ {
+ scheduleRexmit(m_iSndLastAck, first_send_time);
+ }
}
}
}
@@ -12706,7 +13182,10 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime)
const ECheckTimerStage stage = is_fastrexmit ? TEV_CHT_FASTREXMIT : TEV_CHT_REXMIT;
updateCC(TEV_CHECKTIMER, EventVariant(stage));
- m_pMuxer->updateSendNormal(m_parent);
+ if (m_config.uSenderMode == 0)
+ {
+ m_pMuxer->updateSendNormal(m_parent);
+ }
}
void CUDT::checkTimers()
@@ -12739,7 +13218,7 @@ void CUDT::checkTimers()
checkBlindRexmitTimer(currtime);
- if (currtime > m_tsLastSndTime.load() + microseconds_from(COMM_KEEPALIVE_PERIOD_US))
+ if (currtime > m_LastSend.time() + microseconds_from(COMM_KEEPALIVE_PERIOD_US))
{
sendCtrl(UMSG_KEEPALIVE);
#if SRT_ENABLE_BONDING
diff --git a/srtcore/core.h b/srtcore/core.h
index 7f17d7006..1505c4fb4 100644
--- a/srtcore/core.h
+++ b/srtcore/core.h
@@ -69,6 +69,7 @@ modified by
#include "handshake.h"
#include "congctl.h"
#include "packetfilter.h"
+#include "schedule_snd.h"
#include "socketconfig.h"
#include "utilities.h"
@@ -135,6 +136,124 @@ enum SeqPairItems
class CCryptoControl;
+
+class CLastSend
+{
+ friend class CUDT;
+ sync::AtomicClock m_tsSendTime;
+
+ // Statistical data, reset when sending ACKACK
+ sync::AtomicClock m_tsBeginTime;
+ sync::atomic m_uNumberPackets;
+ sync::atomic m_uNumberBytes;
+ uint32_t m_uBytesPerSecond;
+
+public:
+
+ CLastSend(): m_uNumberPackets(0), m_uNumberBytes(0)
+ {
+ }
+
+ sync::steady_clock::time_point time() const { return m_tsSendTime; }
+
+ void collect(const sync::steady_clock::time_point& tm)
+ {
+ using namespace sync;
+
+ steady_clock::time_point old = m_tsBeginTime;
+ m_tsBeginTime.store(m_tsSendTime.load());
+
+ int forespan = 0;
+ if (!is_zero(old))
+ {
+ steady_clock::duration diff = m_tsBeginTime.load() - old;
+ uint64_t basesize = m_uNumberBytes * 1000 * 1000;
+ uint64_t basetime = count_microseconds(diff);
+ if (basetime) // prevent division by 0
+ {
+ m_uBytesPerSecond = basesize / basetime;
+ forespan = 5;
+ }
+ }
+ else
+ {
+ m_uBytesPerSecond = 0;
+ }
+ m_tsSendTime = tm;
+ m_uNumberBytes = 4;
+ m_uNumberPackets = 1;
+
+ steady_clock::duration diff = tm - m_tsBeginTime.load();
+ uint64_t basesize = m_uNumberBytes * 1000 * 1000;
+ uint64_t basetime = count_microseconds(diff);
+ if (basetime) // prevent division by 0
+ m_uBytesPerSecond = (forespan * m_uBytesPerSecond + (basesize / basetime))/(forespan + 1);
+ }
+
+ void update(const sync::steady_clock::time_point& tm, uint32_t bytes, uint32_t npackets = 1)
+ {
+ m_tsSendTime = tm;
+ m_uNumberBytes += +bytes;
+ m_uNumberPackets += +npackets;
+ // Do not calculate speed here. Do it on reset only.
+ }
+};
+
+class CSendPipeManager
+{
+ friend class CUDT;
+ typedef sync::steady_clock::time_point time_point;
+ typedef sync::steady_clock::duration duration;
+
+ //sync::Mutex m_Lock;
+ sync::atomic m_iSchedSeqNo; // SEQNO up to which regular packets were scheduled
+ int32_t m_iBufferedSeqNo; // SEQNO up to which there are packets in the sender buffer
+
+ sync::AtomicClock m_tsTime;
+
+ int m_tdAverageDeviation_us;
+
+public:
+
+ static const int SCHEDULE_FORFEIT_LIMIT_MS = 500;
+
+ CSendPipeManager():
+ m_iSchedSeqNo(SRT_SEQNO_NONE),
+ m_tsTime()
+ {
+ }
+
+ sync::steady_clock::time_point lastTime() const { return m_tsTime.load(); }
+ int32_t lastSchedSeq() const { return m_iSchedSeqNo; }
+
+ bool lazyStart(int32_t seqno, const sync::steady_clock::time_point& tm)
+ {
+ if (m_iSchedSeqNo == SRT_SEQNO_NONE)
+ {
+ m_iSchedSeqNo = seqno;
+ m_tsTime = tm;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // This should not happen, kinda sanity check
+ bool updateFirst(const sync::steady_clock::time_point& currtime)
+ {
+ if (is_zero(m_tsTime) || currtime - m_tsTime.load() > sync::seconds_from(1))
+ {
+ m_tsTime = currtime;
+ return true;
+ }
+ return false;
+ }
+
+ sync::steady_clock::time_point updateNext(const sync::steady_clock::time_point& currtime,
+ const sync::steady_clock::duration& send_interval);
+};
+
class CUDTUnited;
class CUDTSocket;
#if SRT_ENABLE_BONDING
@@ -994,6 +1113,25 @@ class CUDT
SRT_TSA_NEEDS_NONLOCKED(m_ConnectionLock)
void checkSndTimers();
+ // For schedule mode sending
+ sync::steady_clock::time_point lastSchedTime() const { return m_LastSched.lastTime(); }
+ //sync::steady_clock::duration lastSendInterval() const { return m_LastSched.lastInterval(); }
+ void scheduleRegular(int32_t seqno, const time_point& currtime, const time_point& exp_sendtime);
+ void scheduleRexmit(int32_t seqno, const time_point& exp_sendtime);
+
+public: // Used by scheduler
+ bool planSendingTime(sched::Type type, const CUDT::time_point& latest_delivery, CUDT::time_point& w_sendtime);
+ int extractPlannedLoss(SendTaskProto& proto);
+ bool hasSendCapacity() const
+ {
+ const int cwnd = std::min(m_iFlowWindowSize.load(), m_iCongestionWindow.load());
+ const int32_t flight_span = getFlightSpan(m_iSndLastAck.load(), m_iSndCurrSeqNo.load());
+ return cwnd > flight_span;
+ }
+
+ bool isSendReady() const { return !m_CryptoControl.isSndDataGated(); }
+
+private:
/// @brief Check and perform KM refresh if needed.
bool checkSndKMRefresh(int* aw_keyindex);
@@ -1180,7 +1318,7 @@ class CUDT
SRT_TSA_GUARDED_BY(m_RecvAckLock)
atomic_time_point m_tsLastRspTime; // Timestamp of last response from the peer
time_point m_tsLastRspAckTime; // (SND) Timestamp of last ACK from the peer
- atomic_time_point m_tsLastSndTime; // Timestamp of last data/ctrl sent (in system ticks)
+ CLastSend m_LastSend; // Time and stats for the last sending
time_point m_tsLastWarningTime; // Last time that a warning message is sent
atomic_time_point m_tsLastReqTime; // last time when a connection request is sent
time_point m_tsRcvPeerStartTime;
@@ -1192,7 +1330,8 @@ class CUDT
int m_iPktCount; // Packet counter for ACK
int m_iLightACKCount; // Light ACK counter
- time_point m_tsNextSendTime; // Scheduled time of next packet sending
+ time_point m_tsNextSendTime; // Scheduled time of next packet sending (normal mode)
+ CSendPipeManager m_LastSched; // Data for packet scheduling (scheduler mode)
sync::atomic m_iSndLastFullAck; // Last full ACK received
SRT_TSA_GUARDED_BY(m_RecvAckLock)
@@ -1428,6 +1567,14 @@ class CUDT
/// @return payload size on success, <=0 on failure
int packLostData(CSndPacket& packet);
+ /// Pack a specified packet for retransmission. In distinction to the
+ /// version with no number, just get the packet, if it's still eligible.
+ ///
+ /// @param seq [in] the already known sequence to extract
+ /// @param packet [in, out] a packet structure to fill
+ /// @return payload size on success, <=0 on failure
+ int packLostData(int32_t seqno, CSndPacket& packet);
+
duration minRexmitInterval()
{
if (m_bPeerNakReport && m_config.iRetransmitAlgo != 0)
@@ -1444,8 +1591,19 @@ class CUDT
/// Pack a unique data packet (never sent so far) in CPacket for sending.
/// @param packet [in, out] a CPacket structure to fill.
///
- /// @return true if a packet has been packets; false otherwise.
- bool packUniqueData(CSndPacket& packet);
+ /// @return Whether packet packing succeeded and, if not, why it did not.
+ enum PackResult
+ {
+ PACK_OK, // Packet extracted and prepared for sending.
+ PACK_CONGESTED, // A new unscheduled packet is blocked by the flight window or initial key refresh.
+ PACK_UNAVAILABLE, // The socket or scheduled packet is no longer available.
+ PACK_ERROR // An invariant or packet-filter failure.
+ };
+
+ PackResult packUniqueData(CSndPacket& packet);
+ PackResult packUniqueData(int32_t seqno, CSndPacket& packet);
+ PackResult postExtractUniqueData(int current_sequence_number, const time_point& tsOrigin, CSndPacket& packet);
+
/// Pack in CPacket the next data to be send. If the call succeeds, the sequence number
/// of the packet is reserved and guaranteed to stay in the buffer. This should be later
@@ -1460,6 +1618,17 @@ class CUDT
bool packData(CSndPacket& packet, time_point& nexttime, CNetworkInterface& src_addr);
bool releaseSend();
void removeSndLossUpTo(int32_t seq);
+ PackResult packData(const SchedPacket& spec, CSndPacket& packet, CNetworkInterface& src_addr);
+
+ // XXX
+ // previous: time when the previous packet was sent over THIS PIPE.
+ // latest: the latest possible time when the requested packert should be sent
+ // RETURNS: time when the now requested packet should be scheduled
+ // : if zero, this request should be cancelled
+ time_point getNextSendTime(const time_point& previous, const time_point& latest);
+
+ bool defineSchedTimes(int32_t lo, int32_t hi, time_point& w_start, duration& w_step);
+ void scheduleRexmitRange(int32_t lo, int32_t hi);
#if USE_RECEIVER_UNIT_POOL
SRT_TSA_NEEDS_NONLOCKED(m_RcvTsbPdStartupLock, m_StatsLock, m_RecvLock, m_RcvLossLock, m_RcvBufferLock)
diff --git a/srtcore/filelist.maf b/srtcore/filelist.maf
index c0a9365c1..3fb67b6b4 100644
--- a/srtcore/filelist.maf
+++ b/srtcore/filelist.maf
@@ -22,6 +22,7 @@ packet.cpp
packetfilter.cpp
queue.cpp
congctl.cpp
+schedule_snd.cpp
socketconfig.cpp
srt_c_api.cpp
strerror_defs.cpp
@@ -76,6 +77,7 @@ packet.h
sync.h
queue.h
congctl.h
+schedule_snd.h
socketconfig.h
../logging/hvu_compat.h
stats.h
diff --git a/srtcore/queue.cpp b/srtcore/queue.cpp
index 9bcc5f5bc..1abd3de3d 100644
--- a/srtcore/queue.cpp
+++ b/srtcore/queue.cpp
@@ -605,6 +605,7 @@ void CSendOrderList::signalInterrupt()
//
CSndQueue::CSndQueue(CMultiplexer* parent):
m_parent(parent),
+ m_pWorkerFunction(NULL),
m_SendOrderList(parent->m_SocketsLock),
m_pChannel(NULL),
m_bClosing(false)
@@ -621,8 +622,14 @@ void CSndQueue::stop()
{
// We use the decent way, so we say to the thread "please exit".
m_bClosing = true;
-
- m_SendOrderList.signalInterrupt();
+ if (m_pWorkerFunction == &workerSendOrder_fwd)
+ {
+ m_SendOrderList.signalInterrupt();
+ }
+ else if (m_pWorkerFunction == &workerPacketScheduler_fwd)
+ {
+ m_Scheduler.interrupt();
+ }
// Sanity check of the function's affinity.
if (sync::this_thread_is(m_WorkerThread))
@@ -657,12 +664,22 @@ void CSndQueue::init(CChannel* c)
#else
const char* thname = "SRT:SndQ";
#endif
- if (!StartThread((m_WorkerThread), CSndQueue::worker_fwd, this, thname))
+ m_pWorkerFunction = SelectWorkerFunction();
+
+ if (!StartThread((m_WorkerThread), *m_pWorkerFunction, this, thname))
{
throw CUDTException(MJ_SYSTEMRES, MN_THREAD);
}
}
+CSndQueue::worker_fn* CSndQueue::SelectWorkerFunction()
+{
+ if (m_parent->cfg().uSenderMode == 0)
+ return workerSendOrder_fwd;
+
+ return workerPacketScheduler_fwd;
+}
+
#if defined(SRT_DEBUG_SNDQ_HIGHRATE)
static void CSndQueueDebugHighratePrint(const CSndQueue* self, const steady_clock::time_point currtime)
@@ -815,6 +832,118 @@ void CSndQueue::workerSendOrder()
THREAD_EXIT();
}
+void CMultiplexer::scheduleSend(CUDTSocket* src, int32_t seqno, sched::Type type,
+ const sync::steady_clock::time_point& latest_send,
+ const sync::steady_clock::time_point& latest_delivery)
+{
+ if (type == sched::TP_REXMIT)
+ m_SndQueue.m_Scheduler.prescheduleLoss(src, seqno);
+ else
+ m_SndQueue.m_Scheduler.prescheduleRegular(SendTaskProto(SchedPacket(src, seqno, type), latest_send, latest_delivery));
+}
+
+void CMultiplexer::notifyScheduledSendReady()
+{
+ m_SndQueue.m_Scheduler.notifyPendingTasks();
+}
+
+void CSndQueue::workerPacketScheduler()
+{
+ std::string thname;
+ ThreadName::get(thname);
+ THREAD_STATE_INIT(thname.c_str());
+
+ int interrupt_credit = 5;
+ for (;;)
+ {
+ if (m_bClosing)
+ {
+ HLOGC(qslog.Debug, log << "SndQ: closed, exiting");
+ break;
+ }
+
+ HLOGC(qslog.Debug, log << "SndQ: waiting to get next send candidate...");
+ THREAD_PAUSED();
+ //
+ // API Scheduling adds directly to the queue only if it is free.
+ // Otherwise it's added to the "forequeue", with latest sending time.
+ // If the m_Scheduler contains no packets in the past, instead of
+ // waiting YET, you pick the earliest (sorted by expected send time)
+ // packet and enqueue it in the m_Scheduler. At this moment you select
+ // the sending time basing on the currently shaped sending interval.
+
+ // SENDMODE: PLAIN (0): use default order scheduler.
+ // SENDMODE: EAGER (1): schedule packets ASAP, just preserve the timestamp
+ // SENDMODE: PLANNED (2): schedule packets at their declared send time
+ SendTaskProto task;
+ SchedPacket pspec = m_Scheduler.wait_pop(&task);
+ THREAD_RESUMED();
+ INCREMENT_THREAD_ITERATIONS();
+
+ if (pspec.empty())
+ {
+ --interrupt_credit;
+ if (!m_bClosing)
+ {
+ LOGC(qslog.Error, log << "SndQ: IPE: scheduler interrupted while queue is running (tolerate next "
+ << interrupt_credit << ")!");
+ if (!interrupt_credit)
+ {
+ m_bClosing = true;
+ LOGC(qslog.Fatal, log << "SndQ: IPE: closing sender queue to prevent spamming");
+ break;
+ }
+ }
+ continue; // If m_bClosing, the loop will exit at the next iteration
+ }
+
+ CSndPacket sndpkt;
+ CNetworkInterface source_addr;
+ CUDTSocket* s = pspec.m_Socket.socket;
+ const CUDT::PackResult result = s->core().packData(pspec, (sndpkt), (source_addr));
+ if (result == CUDT::PACK_UNAVAILABLE)
+ continue;
+ if (result != CUDT::PACK_OK)
+ {
+ LOGC(qslog.Error, log << "SndQ: IPE: @" << s->id()
+ << " couldn't pack packet for scheduled specification (result=" << result << ")");
+ --interrupt_credit;
+ if (!interrupt_credit)
+ {
+ // XXX Signal broken socket by IPE
+ m_bClosing = true;
+ }
+ continue;
+ }
+ const sockaddr_any target_addr = s->core().m_PeerAddr;
+ m_pChannel->sendto(target_addr, sndpkt.pkt, source_addr);
+
+ steady_clock::time_point last_send_time = steady_clock::now();
+ s->core().m_LastSend.update(last_send_time, sndpkt.pkt.getLength());
+
+ // Restore to maximum after a successful extraction.
+ interrupt_credit = 5;
+
+ /* XXX There's no chained scheduling anymore
+ Any next sending will be based on the direct submission.
+
+ steady_clock::time_point when = s->core().calculateRegularSchedTime();
+
+ // Schedule the next packet, if possible.
+ // If not possible, the next packet will be chained directly
+ // by the API function.
+ CLastSched& sc = s->core().m_LastSched;
+ if (sc.chainSchedule(sndpkt.pkt.getSeqNo(), when))
+ {
+ SendTask task (SchedPacket(s, sc.lastSchedSeq() , sched::TP_REGULAR), when);
+ m_Scheduler.enqueue_task(s->id(), task);
+ }
+ */
+ }
+ THREAD_EXIT();
+}
+
+
void CMultiplexer::removeRID(const SRTSOCKET& id)
{
ScopedLock lkv(m_SocketsLock);
diff --git a/srtcore/queue.h b/srtcore/queue.h
index 8083c9ed5..1c537fe28 100644
--- a/srtcore/queue.h
+++ b/srtcore/queue.h
@@ -60,6 +60,7 @@ modified by
#include "common.h"
#include "packet.h"
#include "socketconfig.h"
+#include "schedule_snd.h"
#include "netinet_any.h"
#include "utilities.h"
@@ -664,6 +665,7 @@ struct CMultiplexer;
class CSndQueue
{
+ friend class TestMockCUDT;
friend class CUDT;
friend class CUDTUnited;
friend struct CMultiplexer;
@@ -691,15 +693,20 @@ class CSndQueue
void stop();
private:
- static void* worker_fwd(void* param)
- {
- CSndQueue* self = (CSndQueue*)param;
- self->workerSendOrder();
+ typedef void* worker_fn(void*);
+#define DEFINE_FWD(classname, name) \
+ static void* name##_fwd(void* param) { classname* self = (classname*)param; self->name(); return NULL; }
- return NULL;
- }
void workerSendOrder();
+ DEFINE_FWD(CSndQueue, workerSendOrder);
+ void workerPacketScheduler();
+ DEFINE_FWD(CSndQueue, workerPacketScheduler);
+
+#undef DEFINE_FWD
+
+ worker_fn* m_pWorkerFunction;
+
sync::CThread m_WorkerThread;
CSendOrderList m_SendOrderList; // List of socket instances for data sending
@@ -707,6 +714,10 @@ class CSndQueue
sync::atomic m_bClosing; // closing the worker
+ SendScheduler m_Scheduler;
+
+ worker_fn* SelectWorkerFunction();
+
public:
bool stopped() const { return !m_WorkerThread.joinable(); }
@@ -899,6 +910,8 @@ struct CMultiplexer
// Dissolution might be a better idea, but this is better for separation.
friend class CSndQueue;
+ friend class TestMockCUDT;
+
int m_iID; // multiplexer ID
mutable sync::Mutex m_SocketsLock;
@@ -1090,6 +1103,14 @@ struct CMultiplexer
void updateSendFast(CUDTSocket* s);
void removeSender(CUDT* u);
+ // SCHEDULER API
+
+ void scheduleSend(CUDTSocket* src, int32_t seqno, sched::Type type,
+ const sync::steady_clock::time_point& when,
+ const sync::steady_clock::time_point& delivery);
+ // Recheck pending packets after a flight-window or key-readiness change.
+ void notifyScheduledSendReady();
+
};
} // namespace srt
diff --git a/srtcore/schedule_snd.cpp b/srtcore/schedule_snd.cpp
new file mode 100644
index 000000000..9cecc0563
--- /dev/null
+++ b/srtcore/schedule_snd.cpp
@@ -0,0 +1,571 @@
+
+#include
+#include
+
+#include "schedule_snd.h"
+#include "api.h"
+#include "core.h"
+#include "logging.h"
+#include "hvu_compat.h"
+#include "ofmt_iostream.h"
+
+using namespace std;
+using namespace srt;
+using namespace srt::sync;
+
+using srt::logging::qslog;
+using namespace hvu;
+
+namespace srt
+{
+
+SchedPacketInfo::SchedPacketInfo(CUDTSocket* sock, int32_t seqno):
+ m_Socket(CUDT::uglobal())
+{
+ if (sock)
+ m_Socket = CUDT::keep(sock);
+
+ m_iSeqNo = seqno;
+}
+
+void SchedPacketInfo::set_socket(CUDTSocket* sock)
+{
+ if (sock)
+ m_Socket = CUDT::keep(sock);
+}
+
+static const char* const schedtype [] = {"regular", "rexmit", "pf-control"};
+
+std::string SendTask::print(SendTask::taskiter_t v)
+{
+ using namespace hvu;
+
+ ofmt_bufs out;
+ // Complicated pre-C++20 time formatting...
+ time_t tval = count_seconds(v->m_tsSendTime.time_since_epoch());
+ struct tm tme = sys_localtime(tval);
+
+ int64_t total_usec = count_microseconds(v->m_tsSendTime.time_since_epoch());
+ int64_t usec = total_usec - (tval * 1000000);
+ out << "<" << fmt(tme, "%T") << "." << fmt(usec, fmtc().width(6).fillzero())
+ << "> @" << v->m_Packet.m_Socket.id();
+ int32_t seq = v->m_Packet.seqno();
+ if (seq == SRT_SEQNO_NONE)
+ {
+ out << " [empty]";
+ }
+ else
+ {
+ out << " [" << schedtype[v->m_Packet.type()] << "] %" << seq;
+ }
+ return out.str();
+}
+
+SendTask::taskrep_t SendScheduler::createTask(const SendTaskProto& sp)
+{
+ SRTSOCKET id = sp.m_Packet.m_Socket.socket->id();
+ SendTask::tasklist_t& wlist = m_TaskMap[id];
+
+ wlist.push_back(SendTask(sp));
+ SendTask::taskiter_t itask = --wlist.end();
+ itask->m_pBaseList = &wlist;
+
+ return SendTask::taskrep_t(wlist, itask);
+}
+
+void SendScheduler::prescheduleLoss(CUDTSocket* provider, int32_t first_seqno)
+{
+ SRT_ASSERT(provider);
+ if (!provider)
+ {
+ LOGC(qslog.Error, log << "Schedule: PRE-ENQ: cannot schedule loss without a socket");
+ return;
+ }
+
+ if (m_bBroken)
+ {
+ HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: DENIED, schedule is broken");
+ return;
+ }
+
+ SRTSOCKET id = provider->id();
+ sync::ScopedLock lk (m_Lock);
+
+ SchedPacketInfo* psi;
+ bool isnew;
+ Tie(psi, isnew) = map_tryinsertp(m_PendingRexmit, id);
+
+ if (!isnew)
+ {
+ HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: @" << id << " LOSS first=%" << first_seqno
+ << ": ALREADY SCHEDULED, skipping");
+ return;
+ }
+
+ IF_HEAVY_LOGGING(const char* notif = "");
+ // Insert a new item
+ psi->set_socket(provider);
+ SRT_ASSERT(psi->m_Socket.socket);
+ if (!psi->m_Socket.socket)
+ {
+ LOGC(qslog.Error, log << "Schedule: PRE-ENQ: failed to keep socket @" << id << " for loss scheduling");
+ m_PendingRexmit.erase(id);
+ return;
+ }
+ psi->m_iSeqNo = first_seqno;
+
+ if (!have_task_ready())
+ {
+ m_TaskReadyCond.notify_all();
+ IF_HEAVY_LOGGING(notif = " [NOTIFIED]");
+ }
+
+ HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: @" << id << " LOSS first=%" << first_seqno
+ << ": newly scheduled " << notif);
+}
+
+void SendScheduler::prescheduleRegular(const SendTaskProto& sp)
+{
+ SRT_ASSERT(sp.m_Packet.m_Socket.socket);
+ if (!sp.m_Packet.m_Socket.socket)
+ {
+ LOGC(qslog.Error, log << "Schedule: PRE-ENQ: cannot schedule regular packet without a socket");
+ return;
+ }
+
+ if (m_bBroken)
+ {
+ HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: DENIED, schedule is broken");
+ return;
+ }
+
+
+ sync::ScopedLock lk (m_Lock);
+ SendTask::taskrep_t rtask = createTask(sp);
+
+ m_PendingRegularQueue.push_back(rtask);
+
+ IF_HEAVY_LOGGING(sched::Type type = sp.m_Packet.m_Type);
+ IF_HEAVY_LOGGING(SRTSOCKET id = sp.m_Packet.m_Socket.socket->id());
+ IF_HEAVY_LOGGING(const char* notif = "");
+ IF_HEAVY_LOGGING(static const char* const type_name[3] = {"REGULAR", "REXMIT", "CONTROL"});
+ if (!have_task_ready())
+ {
+ m_TaskReadyCond.notify_all();
+ IF_HEAVY_LOGGING(notif = " [NOTIFIED]");
+ }
+
+ HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: @" << id << " %" << sp.m_Packet.m_iSeqNo << " type=" << type_name[type]
+ << "LATEST: SEND=" << FormatTime(sp.m_tsSendTime) << " DELIVERY=" << FormatTime(sp.m_tsLatestDeliveryTime) << notif);
+}
+
+// NOTE: This is a function that enqueues a task in the queue directly;
+// that should be rather not used because you need to know the exact execution time.
+ /*
+SendTask::taskiter_t SendScheduler::enqueue_task(socket_t id, const SchedPacket& proto, const clock_time& when, const clock_time& delivery)
+{
+ if (m_bBroken)
+ {
+ HLOGC(qslog.Debug, log << "Schedule: ENQ: DENIED, schedule is broken");
+ return SendTask::none();
+ }
+
+ sync::ScopedLock lk (m_Lock);
+ SendTask::taskiter_t itask = create_task(proto, when, delivery);
+
+ bool was_ready = have_task_ready();
+
+ size_t pos = m_TaskQueue.insert(itask);
+
+ IF_HEAVY_LOGGING(bool was_first = false);
+ IF_HEAVY_LOGGING(bool now_ready = false);
+ if (pos == 0) // earliest task
+ {
+ m_tsAboutTime = m_TaskQueue.top()->m_tsSendTime; // INSERTED: will not be empty
+ IF_HEAVY_LOGGING(was_first = true);
+ }
+
+ // XXX Shouldn't it update always if m_tsAboutTime was updated?
+ if (!was_ready && have_task_ready())
+ {
+ m_TaskReadyCond.notify_all();
+ IF_HEAVY_LOGGING(now_ready = true);
+ }
+
+ HLOGC(qslog.Debug, log << "Schedule: ENQ: new"
+ << fmt_if(now_ready, " READY")
+ << " task at T=" << FormatTime(itask->m_tsSendTime)
+ << fmt_if(was_first, " (NEW TOP)")
+ << fmt_if(was_ready, " (NOW READY)", " (ONLY ADDED)")
+ << fmt_if(now_ready, " - NOTIFY"));
+
+ return itask;
+}
+// */
+
+bool SendScheduler::updatePreschedule(size_t max_sched)
+{
+ // The next call to wait() will return an already ready task,
+ // so do not reschedule new ones until they are all dispatched.
+ if (have_task_ready())
+ {
+ HLOGC(qslog.Debug, log << "updatePreschedule: POSTPONED, have ready tasks");
+ return true;
+ }
+
+ if (m_PendingRegularQueue.empty() && m_PendingRexmit.empty())
+ {
+ HLOGC(qslog.Debug, log << "updatePreschedule: NO PENDING tasks");
+ return false;
+ }
+
+ steady_clock::time_point now = steady_clock::now();
+
+ // Prioritization:
+ // 1. Check if there is any regular packet already overdue. If so,
+ // it takes precedence before any retransmission packet.
+ size_t nsched = 0;
+ std::map blocked;
+ for (pending_t::iterator it = m_PendingRegularQueue.begin(); it != m_PendingRegularQueue.end();)
+ {
+ SendTask::taskrep_t pt = *it;
+ if (pt->m_tsSendTime > now)
+ break; // stop at first being in the future
+
+ const socket_t id = pt->m_Packet.id();
+ if (blocked.find(id) != blocked.end())
+ {
+ ++it;
+ continue;
+ }
+ CUDT& core = pt->m_Packet.m_Socket.socket->core();
+ if (!core.isSendReady() || !core.hasSendCapacity())
+ {
+ blocked[id] = true;
+ ++it;
+ continue;
+ }
+
+ it = m_PendingRegularQueue.erase(it);
+
+ SchedPacket& proto = pt->m_Packet;
+ proto.m_Socket.socket->core().planSendingTime(proto.m_Type, pt->m_tsLatestDeliveryTime, (pt->m_tsSendTime));
+ size_t pos = m_TaskQueue.insert(pt);
+
+ if (pos == 0) // earliest task
+ {
+ // INSERTED: will not be empty
+ m_tsAboutTime = m_TaskQueue.top_raw()->m_tsSendTime;
+ // NOTE: theoretically you should notify() here, but it's not necessary
+ // because this function has the same affinity as the wait() caller.
+ }
+
+ ++nsched;
+ if (nsched >= max_sched)
+ {
+ HLOGC(qslog.Debug, log << "updatePreschedule: enqueued " << max_sched << " REGULAR tasks, "
+ << fmt_if(pos == 0, " NEW HEAD,") << " still pending: "
+ << m_PendingRegularQueue.size() << " REG + " << m_PendingRexmit.size() << " REXMIT");
+ return have_task_ready();
+ }
+ }
+
+ // 2. Review the retransmission packets; insert those that still have
+ // remaining delivery time. Drop the others.
+
+ IF_HEAVY_LOGGING(int regsched = nsched);
+ IF_HEAVY_LOGGING(bool new_head = false);
+ std::vector to_delete;
+
+ // Ok, the rexmit queue is different.
+ // We have normally information about ONE loss, which at the moment of
+ // pickup need not hold. We then run the loop by index, with notifying up
+ // to which element we have passed the task to the task queue, and those
+ // will be then removed.
+
+ // Walk through all entries. Later delete those that don't report any more lost packets
+ for (map::const_iterator i = m_PendingRexmit.begin(); i != m_PendingRexmit.end(); ++i)
+ {
+ SendTaskProto proto;
+
+ for (;;)
+ {
+ // This should check what losses are expected to be sent, which losses
+ // are too old and should be deleted, and whether anything is still about to be sent.
+ int loss_chain = i->second.m_Socket.socket->core().extractPlannedLoss((proto));
+ // RETURNS:
+ // 0: No loss scheduled at the moment for that iteration (note: should not happen IN ITERATION)
+ // 1: One loss was detected, but then there is none.
+ // 2: The loss was extracted and there's a new one waiting.
+ if (loss_chain)
+ {
+ if (!proto.m_Packet.empty())
+ {
+ SRT_ASSERT(proto.m_Packet.m_Socket.socket);
+ if (!proto.m_Packet.m_Socket.socket)
+ {
+ LOGC(qslog.Error, log << "Schedule: PRE-ENQ: loss extraction returned a packet without a kept socket");
+ to_delete.push_back(i->first);
+ break;
+ }
+
+ ++nsched;
+ // We have something - schedule it.
+ SendTask::taskrep_t itask = createTask(proto);
+
+ size_t pos = m_TaskQueue.insert(itask);
+ if (pos == 0) // earliest task
+ {
+ m_tsAboutTime = m_TaskQueue.top_raw()->m_tsSendTime;
+ IF_HEAVY_LOGGING(new_head = true);
+ }
+ }
+ }
+
+ if (loss_chain != 2)
+ {
+ to_delete.push_back(i->first);
+ break; // forget about this one; check the next socket
+ }
+
+ if (nsched >= max_sched)
+ {
+ HLOGC(qslog.Debug, log << "updatePreschedule: enqueued " << max_sched << " = " << regsched
+ << " REG + " << (nsched - regsched) << " REXMIT tasks,"
+ << fmt_if(new_head, " NEW HEAD,") << " still pending: "
+ << m_PendingRegularQueue.size() << " REG + " << m_PendingRexmit.size() << " sockets in REXMIT");
+ return have_task_ready();
+ }
+ }
+
+ if (nsched >= max_sched)
+ {
+ return have_task_ready();
+ }
+ }
+
+ // Delete entries for sockets that do not report any rexmit
+ for (size_t i = 0; i < to_delete.size(); ++i)
+ m_PendingRexmit.erase(to_delete[i]);
+
+ IF_HEAVY_LOGGING(int rexsched = nsched);
+ // 3. Try to schedule regular packets, if there are still free slots
+ for (pending_t::iterator it = m_PendingRegularQueue.begin(); it != m_PendingRegularQueue.end();)
+ {
+ SendTask::taskrep_t pt = *it;
+ const socket_t id = pt->m_Packet.id();
+ if (blocked.find(id) != blocked.end())
+ {
+ ++it;
+ continue;
+ }
+ CUDT& core = pt->m_Packet.m_Socket.socket->core();
+ if (!core.isSendReady() || !core.hasSendCapacity())
+ {
+ blocked[id] = true;
+ ++it;
+ continue;
+ }
+
+ it = m_PendingRegularQueue.erase(it);
+ SchedPacket& proto = pt->m_Packet;
+ proto.m_Socket.socket->core().planSendingTime(proto.m_Type, pt->m_tsLatestDeliveryTime, (pt->m_tsSendTime));
+ size_t pos = m_TaskQueue.insert(pt);
+
+ if (pos == 0) // earliest task
+ {
+ // INSERTED: will not be empty
+ m_tsAboutTime = m_TaskQueue.top_raw()->m_tsSendTime;
+ // NOTE: theoretically you should notify() here, but it's not necessary
+ // because this function has the same affinity as the wait() caller.
+ IF_HEAVY_LOGGING(new_head = true);
+ }
+
+ ++nsched;
+ if (nsched >= max_sched)
+ {
+ break;
+ }
+ }
+
+ HLOGC(qslog.Debug, log << "updatePreschedule: enqueued " << nsched << " = "
+ << regsched << "YD + " << (rexsched - regsched) << "RX + "
+ << (nsched - rexsched) << "RG tasks,"
+ << fmt_if(new_head, " NEW HEAD,") << " still pending: "
+ << m_PendingRegularQueue.size() << " REG + " << m_PendingRexmit.size() << " sockets in REXMIT");
+ return have_task_ready();
+}
+
+bool SendScheduler::have_task_ready()
+{
+ // XXX Make sure it works. The m_tsAboutTime is being updated
+ // with every update in m_Scheduler, so it should keep the top
+ // element's time, or be 0 if there's no top element.
+
+ return !sync::is_zero(m_tsAboutTime) && m_tsAboutTime <= clock_type::now();
+#if 0
+
+ if (!m_TaskQueue.empty())
+ {
+ SendTask::taskiter_t earliest = m_TaskQueue.top();
+ if (earliest->is_ready(clock_type::now()))
+ {
+ return true;
+ }
+ }
+ return false;
+#endif
+}
+
+void SendScheduler::withdraw(socket_t id)
+{
+ sync::ScopedLock lk (m_Lock);
+ // Delete all tasks for the given socket id.
+ // We have them collected in the list: m_TaskMap
+
+ SendTask::tasklist_t& id_list = m_TaskMap[id];
+
+ // As we know that all these items were added to m_TaskQueue,
+ // we need to withdraw them all from m_TaskQueue.
+
+ IF_HEAVY_LOGGING(int nerased = 0);
+ for (SendTask::taskiter_t idt = id_list.begin(); idt != id_list.end(); ++idt)
+ {
+ if (m_TaskQueue.erase(SendTask::taskrep_t(id_list, idt)))
+ {
+ IF_HEAVY_LOGGING(++nerased);
+ }
+ }
+ // The list should be empty, so delete the entry itself.
+ int iderased SRT_ATR_UNUSED = m_TaskMap.erase(id);
+ // We don't know if the earliest in the queue was deleted,
+ // so just rewrite it anyway.
+ pop_update_time();
+
+#if HVU_ENABLE_HEAVY_LOGGING
+ hvu::ofmt_bufs nextone;
+ if (m_TaskQueue.empty())
+ nextone << "NO NEXT TASK";
+ else
+ nextone << "next in " << FormatDurationAuto(m_tsAboutTime - steady_clock::now())
+ << " from @" << m_TaskQueue.top()->m_Packet.id();
+#endif
+ HLOGC(qslog.Debug, log << "Schedule: withdrawn @" << int(id)
+ << (iderased ? "" : " (NOT FOUND!)") << " - erased " << nerased << " tasks -" << nextone);
+}
+
+void SendScheduler::pop_update_time()
+{
+ if (!m_TaskQueue.empty())
+ m_tsAboutTime = m_TaskQueue.top()->m_tsSendTime; // checked that ! empty
+ else
+ m_tsAboutTime = clock_time();
+}
+
+void SendScheduler::cancel(SendTask::taskrep_t itask)
+{
+ sync::ScopedLock lk (m_Lock);
+ cancel_nolock(itask);
+}
+
+void SendScheduler::interrupt()
+{
+ m_bBroken = true;
+ HLOGC(qslog.Debug, log << "Schedule: INTERRUPT: locking...");
+ sync::ScopedLock hold (m_Lock);
+ HLOGC(qslog.Debug, log << "Schedule: INTERRUPT: notifying waiters");
+
+ m_TaskReadyCond.notify_all(); // Force waiting functions to exit
+}
+
+void SendScheduler::notifyPendingTasks()
+{
+ sync::ScopedLock hold (m_Lock);
+ m_TaskReadyCond.notify_all();
+}
+
+
+void SendScheduler::cancel_nolock(SendTask::taskrep_t itask)
+{
+ HLOGC(qslog.Debug, log << "Schedule: CANCEL: @" << itask->m_Packet.id() << " T=" << FormatTime(itask->m_tsSendTime));
+ m_TaskQueue.erase(itask);
+ itask->m_pBaseList->erase(itask.get());
+ pop_update_time();
+}
+
+SchedPacket SendScheduler::wait_pop(SendTaskProto* task)
+{
+ SchedPacket packet;
+ sync::UniqueLock lk (m_Lock);
+ // Wait until the time has come to execute
+ // the next task. Extract the task structure
+ // and remove the task from the list.
+
+ for (;;)
+ {
+ if (m_bBroken)
+ {
+ HLOGC(qslog.Debug, log << "Schedule: wait_pop: broken");
+ return SchedPacket();
+ }
+
+ // This will move pending tasks if there's no ready task yet.
+ // Returns true if there's at least one task ready for pickup
+ if (updatePreschedule(4 /*XXX USE CONSTANT*/))
+ {
+ HLOGC(qslog.Debug, log << "Schedule: WAIT: task ready since " << FormatDurationAuto(steady_clock::now() - m_tsAboutTime));
+ break;
+ }
+
+ if (m_TaskQueue.empty()) // This means that also m_ForeQueue was empty
+ {
+ HLOGC(qslog.Debug, log << "Schedule: WAIT: task NOT ready, NO NEW TASKS, WAIT FOR SIGNAL)");
+ // This will be signaled if anything is added to m_ForeQueue or m_TaskQueue.
+ m_TaskReadyCond.wait(lk);
+ }
+ else
+ {
+ clock_time now = steady_clock::now();
+ HLOGC(qslog.Debug, log << "Schedule: WAIT: task NOT ready, next in " << FormatDurationAuto(m_tsAboutTime - now));
+
+ duration remain = m_tsAboutTime - now;
+ if (remain > duration()) // m_tsAboutTime is in the future
+ {
+ // Limit long waits to 3/4 of the remaining time so pending work
+ // and capacity notifications can wake the scheduler promptly.
+ if (remain > sync::milliseconds_from(10))
+ m_TaskReadyCond.wait_for(lk, remain * 3 / 4);
+ else
+ m_TaskReadyCond.wait_for(lk, milliseconds_from(1));
+ }
+ }
+ // CONTINUE.
+ }
+ // Here we are sure that the top() task is ready to execute
+ SendTask::taskiter_t itask = m_TaskQueue.pop();
+ pop_update_time();
+
+ if (itask == SendTask::none())
+ {
+ HLOGC(qslog.Debug, log << "Schedule: wait_pop: IPE: THE QUEUE IS EMPTY");
+ return SchedPacket();
+ }
+ // The node is already removed from the heapset.
+
+ // Extract the required data
+ packet = itask->m_Packet;
+ if (task)
+ *task = SendTaskProto(itask->m_Packet, itask->m_tsSendTime, itask->m_tsLatestDeliveryTime);
+
+ // Now remove it from the corresponding list.
+ itask->m_pBaseList->erase(itask);
+
+ IF_HEAVY_LOGGING(static string typenames[3] = {"REGULAR", "REXMIT", "CONTROL"});
+ HLOGC(qslog.Debug, log << "Schedule: wait_pop: PICKUP from @" << packet.id()
+ << " %" << packet.seqno()
+ << " type=" << typenames[packet.type()]);
+
+ return packet;
+}
+
+}
diff --git a/srtcore/schedule_snd.h b/srtcore/schedule_snd.h
new file mode 100644
index 000000000..a7dc1cc09
--- /dev/null
+++ b/srtcore/schedule_snd.h
@@ -0,0 +1,279 @@
+
+#ifndef INC_SRT_SCHEDULE_SND_H
+#define INC_SRT_SCHEDULE_SND_H
+
+#include