示例#1
0
文件: tomo_seq.py 项目: kojikoji/stge
 def get_slice_mean_axis(self, point_mat, axis):
     from utils import safe_mean
     slice_mean = np.array(
         [safe_mean(point_mat[idx_vec, axis])
          for idx_vec in self.slice_idx_mat])
     return(slice_mean)
示例#2
0
def parse_opened_exp(exp, exp_flp, exp_dir, out_flp, skip_smoothed):
    """ Parses an experiment. Returns the smallest safe window size. """
    print(f"Parsing: {exp_flp}")
    if exp.name.startswith("FAILED"):
        print(f"Error: Experimant failed: {exp_flp}")
        return -1
    if exp.tot_flws == 0:
        print(f"Error: No flows to analyze in: {exp_flp}")
        return -1

    # Determine flow src and dst ports.
    params_flp = path.join(exp_dir, f"{exp.name}.json")
    if not path.exists(params_flp):
        print(f"Error: Cannot find params file ({params_flp}) in: {exp_flp}")
        return -1
    with open(params_flp, "r") as fil:
        params = json.load(fil)
    # Dictionary mapping a flow to its flow's CCA. Each flow is a tuple of the
    # form: (client port, server port)
    #
    # { (client port, server port): CCA }
    flw_to_cca = {(client_port, flw[4]): flw[0]
                  for flw in params["flowsets"] for client_port in flw[3]}
    flws = list(flw_to_cca.keys())

    client_pcap = path.join(exp_dir, f"client-tcpdump-{exp.name}.pcap")
    server_pcap = path.join(exp_dir, f"server-tcpdump-{exp.name}.pcap")
    if not (path.exists(client_pcap) and path.exists(server_pcap)):
        print(f"Warning: Missing pcap file in: {exp_flp}")
        return -1
    flw_to_pkts_client = utils.parse_packets(client_pcap, flw_to_cca)
    flw_to_pkts_server = utils.parse_packets(server_pcap, flw_to_cca)

    # Determine the path to the bottleneck queue log file.
    toks = exp.name.split("-")
    q_log_flp = path.join(
        exp_dir,
        "-".join(toks[:-1]) + "-forward-bottleneckqueue-" + toks[-1] + ".log")
    q_log = None
    if path.exists(q_log_flp):
        q_log = list(enumerate(utils.parse_queue_log(q_log_flp)))

    # Transform absolute times into relative times to make life easier.
    #
    # Determine the absolute earliest time observed in the experiment.
    earliest_time_us = min(first_time_us for bounds in [
        get_time_bounds(flw_to_pkts_client, direction="data"),
        get_time_bounds(flw_to_pkts_client, direction="ack"),
        get_time_bounds(flw_to_pkts_server, direction="data"),
        get_time_bounds(flw_to_pkts_server, direction="ack")
    ] for first_time_us, _ in bounds)
    # Subtract the earliest time from all times.
    for flw in flws:
        flw_to_pkts_client[flw][0][
            features.ARRIVAL_TIME_FET] -= earliest_time_us
        flw_to_pkts_client[flw][1][
            features.ARRIVAL_TIME_FET] -= earliest_time_us
        flw_to_pkts_server[flw][0][
            features.ARRIVAL_TIME_FET] -= earliest_time_us
        flw_to_pkts_server[flw][1][
            features.ARRIVAL_TIME_FET] -= earliest_time_us

        assert (flw_to_pkts_client[flw][0][features.ARRIVAL_TIME_FET] >=
                0).all()
        assert (flw_to_pkts_client[flw][1][features.ARRIVAL_TIME_FET] >=
                0).all()
        assert (flw_to_pkts_server[flw][0][features.ARRIVAL_TIME_FET] >=
                0).all()
        assert (flw_to_pkts_server[flw][1][features.ARRIVAL_TIME_FET] >=
                0).all()

    flws_time_bounds = get_time_bounds(flw_to_pkts_server, direction="data")

    # Process PCAP files from senders and receivers.
    # The final output, with one entry per flow.
    flw_results = {}

    # Keep track of the number of erroneous throughputs (i.e., higher than the
    # experiment bandwidth) for each window size.
    win_to_errors = {win: 0 for win in features.WINDOWS}

    # Create the (super-complicated) dtype. The dtype combines each metric at
    # multiple granularities.
    dtype = (features.REGULAR +
             ([] if skip_smoothed else features.make_smoothed_features()))

    for flw_idx, flw in enumerate(flws):
        cca = flw_to_cca[flw]
        # Copa and PCC Vivace use packet-based sequence numbers as opposed to
        # TCP's byte-based sequence numbers.
        packet_seq = cca in {"copa", "vivace"}
        snd_data_pkts, snd_ack_pkts = flw_to_pkts_client[flw]
        recv_data_pkts, recv_ack_pkts = flw_to_pkts_server[flw]

        first_data_time_us = recv_data_pkts[0][features.ARRIVAL_TIME_FET]

        # The final output. -1 implies that a value could not be calculated.
        output = np.full(len(recv_data_pkts), -1, dtype=dtype)

        # If this flow does not have any packets, then skip it.
        skip = False
        if snd_data_pkts.shape[0] == 0:
            skip = True
            print(f"Warning: No data packets sent for flow {flw_idx} in: "
                  f"{exp_flp}")
        if recv_data_pkts.shape[0] == 0:
            skip = True
            print(f"Warning: No data packets received for flow {flw_idx} in: "
                  f"{exp_flp}")
        if recv_ack_pkts.shape[0] == 0:
            skip = True
            print(f"Warning: No ACK packets sent for flow {flw_idx} in: "
                  f"{exp_flp}")
        if skip:
            flw_results[flw] = output
            continue

        # State that the windowed metrics need to track across packets.
        win_state = {
            win: {
                # The index at which this window starts.
                "window_start_idx": 0,
                # The "loss event rate".
                "loss_interval_weights": make_interval_weight(8),
                "loss_event_intervals": collections.deque(),
                "current_loss_event_start_idx": 0,
                "current_loss_event_start_time": 0,
            }
            for win in features.WINDOWS
        }
        # Total number of packet losses up to the current received
        # packet.
        pkt_loss_total_estimate = 0
        # Loss rate estimation.
        prev_seq = None
        prev_payload_B = None
        highest_seq = None
        # Use for Copa RTT estimation.
        snd_ack_idx = 0
        snd_data_idx = 0
        # Use for TCP and PCC Vivace RTT estimation.
        recv_ack_idx = 0

        # Track which packets are definitely retransmissions. Ignore these
        # packets when estimating the RTT. Note that because we are doing
        # receiver-side retransmission tracking, it is possible that there are
        # other retransmissions that we cannot detect.
        #
        # All sequence numbers that have been received.
        unique_pkts = set()
        # Sequence numbers that have been received multiple times.
        retrans_pkts = set()

        for j, recv_pkt in enumerate(recv_data_pkts):
            if j % 1000 == 0:
                print(f"\tFlow {flw_idx + 1}/{exp.tot_flws}: "
                      f"{j}/{len(recv_data_pkts)} packets")
            # Whether this is the first packet.
            first = j == 0
            # Note that Copa and Vivace use packet-level sequence numbers
            # instead of TCP's byte-level sequence numbers.
            recv_seq = recv_pkt[features.SEQ_FET]
            output[j][features.SEQ_FET] = recv_seq
            retrans = (recv_seq in unique_pkts or
                       (prev_seq is not None and prev_payload_B is not None and
                        (prev_seq +
                         (1 if packet_seq else prev_payload_B)) > recv_seq))
            if retrans:
                # If this packet is a multiple retransmission, then this line
                # has no effect.
                retrans_pkts.add(recv_seq)
            # If this packet has already been seen, then this line has no
            # effect.
            unique_pkts.add(recv_seq)

            recv_time_cur_us = recv_pkt[features.ARRIVAL_TIME_FET]
            output[j][features.ARRIVAL_TIME_FET] = recv_time_cur_us

            payload_B = recv_pkt[features.PAYLOAD_FET]
            wirelen_B = recv_pkt[features.WIRELEN_FET]
            output[j][features.PAYLOAD_FET] = payload_B
            output[j][features.WIRELEN_FET] = wirelen_B
            output[j][features.TOTAL_SO_FAR_FET] = (
                (0 if first else output[j - 1][features.TOTAL_SO_FAR_FET]) +
                wirelen_B)
            output[j][features.PAYLOAD_SO_FAR_FET] = (
                (0 if first else output[j - 1][features.PAYLOAD_SO_FAR_FET]) +
                payload_B)

            # Count how many flows were active when this packet was captured.
            active_flws = sum(
                1 for first_time_us, last_time_us in flws_time_bounds
                if first_time_us <= recv_time_cur_us <= last_time_us)
            assert active_flws > 0, \
                (f"Error: No active flows detected for packet {j} of "
                 f"flow {flw_idx} in: {exp_flp}")

            output[j][features.ACTIVE_FLOWS_FET] = active_flws
            output[j][features.BW_FAIR_SHARE_FRAC_FET] = utils.safe_div(
                1, active_flws)
            output[j][features.BW_FAIR_SHARE_BPS_FET] = utils.safe_div(
                exp.bw_bps, active_flws)

            # Calculate RTT-related metrics.
            rtt_us = -1
            if not first and recv_seq != -1 and not retrans:
                if cca == "copa":
                    # In a Copa ACK, the sender timestamp is the time at which
                    # the corresponding data packet was sent. The receiver
                    # timestamp is the time that the data packet was received
                    # and the ACK was sent. This enables sender-side RTT
                    # estimation. However, because the sender does not echo a
                    # value back to the receiver, this cannot be used for
                    # receiver-size RTT estimation.
                    #
                    # For now, we will just do sender-side RTT estimation. When
                    # selecting which packets to use for the RTT estimate, we
                    # will select the packet/ACK pair whose ACK arrived soonest
                    # before packet j was sent. This means that the sender would
                    # have been able to calculate this RTT estimate before
                    # sending packet j, and could very well have included the
                    # RTT estimate in packet j's header.
                    #
                    # First, find the index of the ACK that was received soonest
                    # before packet j was sent.
                    snd_ack_idx = utils.find_bound(
                        snd_ack_pkts[features.SEQ_FET],
                        recv_seq,
                        snd_ack_idx,
                        snd_ack_pkts.shape[0] - 1,
                        which="before")
                    snd_ack_seq = snd_ack_pkts[snd_ack_idx][features.SEQ_FET]
                    # Then, find this ACK's data packet.
                    snd_data_seq = snd_data_pkts[snd_data_idx][
                        features.SEQ_FET]
                    while snd_data_idx < snd_data_pkts.shape[0]:
                        snd_data_seq = snd_data_pkts[snd_data_idx][
                            features.SEQ_FET]
                        if snd_data_seq == snd_ack_seq:
                            # Third, the RTT is the difference between the
                            # sending time of the data packet and the arrival
                            # time of its ACK.
                            rtt_us = (snd_ack_pkts[snd_ack_idx][
                                features.ARRIVAL_TIME_FET] -
                                      snd_data_pkts[snd_data_idx][
                                          features.ARRIVAL_TIME_FET])
                            assert rtt_us >= 0, \
                                (f"Error: Calculated negative RTT ({rtt_us} "
                                 f"us) for packet {j} of flow {flw} in: "
                                 f"{exp_flp}")
                            break
                        snd_data_idx += 1
                elif cca == "vivace":
                    # UDT ACKs may contain the RTT. Find the last ACK to be sent
                    # by the receiver before packet j was received.
                    recv_ack_idx = utils.find_bound(
                        recv_ack_pkts[features.ARRIVAL_TIME_FET],
                        recv_time_cur_us,
                        recv_ack_idx,
                        recv_ack_pkts.shape[0] - 1,
                        which="before")
                    udt_rtt_us = recv_ack_pkts[recv_ack_idx][features.TS_1_FET]
                    if udt_rtt_us > 0:
                        # The RTT is an optional field in UDT ACK packets. I
                        # assume that this means that if the RTT is not
                        # included, then the field will be 0.
                        rtt_us = udt_rtt_us
                else:
                    # This is a TCP flow. Do receiver-side RTT estimation using
                    # the TCP timestamp option. Attempt to find a new RTT
                    # estimate. Move recv_ack_idx to the first occurance of the
                    # timestamp option TSval corresponding to the current
                    # packet's TSecr.
                    recv_ack_idx_old = recv_ack_idx
                    tsval = recv_ack_pkts[recv_ack_idx][features.TS_1_FET]
                    tsecr = recv_pkt[features.TS_2_FET]
                    while recv_ack_idx < recv_ack_pkts.shape[0]:
                        tsval = recv_ack_pkts[recv_ack_idx][features.TS_1_FET]
                        if tsval == tsecr:
                            # If we found a timestamp option match, then update
                            # the RTT estimate.
                            rtt_us = (recv_time_cur_us -
                                      recv_ack_pkts[recv_ack_idx][
                                          features.ARRIVAL_TIME_FET])
                            break
                        recv_ack_idx += 1
                    else:
                        # If we never found a matching tsval, then use the
                        # previous RTT estimate and reset recv_ack_idx to search
                        # again on the next packet.
                        rtt_us = output[j - 1][features.RTT_FET]
                        recv_ack_idx = recv_ack_idx_old

            recv_time_prev_us = (-1 if first else
                                 output[j - 1][features.ARRIVAL_TIME_FET])
            interarr_time_us = utils.safe_sub(recv_time_cur_us,
                                              recv_time_prev_us)
            output[j][features.INTERARR_TIME_FET] = interarr_time_us
            output[j][features.INV_INTERARR_TIME_FET] = utils.safe_mul(
                8 * 1e6 * wirelen_B, utils.safe_div(1, interarr_time_us))

            output[j][features.RTT_FET] = rtt_us
            min_rtt_us = utils.safe_min(
                sys.maxsize if first else output[j - 1][features.MIN_RTT_FET],
                rtt_us)
            output[j][features.MIN_RTT_FET] = min_rtt_us
            rtt_estimate_ratio = utils.safe_div(rtt_us, min_rtt_us)
            output[j][features.RTT_RATIO_FET] = rtt_estimate_ratio

            # Receiver-side loss rate estimation. Estimate the number of lost
            # packets since the last packet. Do not try anything complex or
            # prone to edge cases. Consider only the simple case where the last
            # packet and current packet are in order and not retransmissions.
            pkt_loss_cur_estimate = (
                -1 if (recv_seq == -1 or prev_seq is None or prev_seq == -1
                       or prev_payload_B is None or prev_payload_B <= 0
                       or payload_B <= 0 or highest_seq is None or
                       # The last packet was a retransmission.
                       highest_seq != prev_seq or
                       # The current packet is a retransmission.
                       retrans) else round(
                           (recv_seq -
                            (1 if packet_seq else prev_payload_B) - prev_seq) /
                           (1 if packet_seq else payload_B)))

            if pkt_loss_cur_estimate != -1:
                pkt_loss_total_estimate += pkt_loss_cur_estimate
            loss_rate_cur = utils.safe_div(
                pkt_loss_cur_estimate, utils.safe_add(pkt_loss_cur_estimate,
                                                      1))

            output[j][features.PACKETS_LOST_FET] = pkt_loss_cur_estimate
            output[j][features.LOSS_RATE_FET] = loss_rate_cur

            # EWMA metrics.
            for (metric,
                 _), alpha in itertools.product(features.EWMAS,
                                                features.ALPHAS):
                if skip_smoothed:
                    continue

                metric = features.make_ewma_metric(metric, alpha)
                if metric.startswith(features.INTERARR_TIME_FET):
                    new = interarr_time_us
                elif metric.startswith(features.INV_INTERARR_TIME_FET):
                    # Do not use the interarrival time EWMA to calculate the
                    # inverse interarrival time. Instead, use the true inverse
                    # interarrival time so that the value used to update the
                    # inverse interarrival time EWMA is not "EWMA-ified" twice.
                    new = output[j][features.INV_INTERARR_TIME_FET]
                elif metric.startswith(features.RTT_FET):
                    new = rtt_us
                elif metric.startswith(features.RTT_RATIO_FET):
                    new = rtt_estimate_ratio
                elif metric.startswith(features.LOSS_RATE_FET):
                    new = loss_rate_cur
                elif metric.startswith(features.MATHIS_TPUT_FET):
                    # tput = (MSS / RTT) * (C / sqrt(p))
                    new = utils.safe_mul(
                        utils.safe_div(
                            utils.safe_mul(8, output[j][features.PAYLOAD_FET]),
                            utils.safe_div(output[j][features.RTT_FET], 1e6)),
                        utils.safe_div(MATHIS_C,
                                       utils.safe_sqrt(loss_rate_cur)))
                else:
                    raise Exception(f"Unknown EWMA metric: {metric}")
                # Update the EWMA. If this is the first value, then use 0 are
                # the old value.
                output[j][metric] = utils.safe_update_ewma(
                    -1 if first else output[j - 1][metric], new, alpha)

            # If we cannot estimate the min RTT, then we cannot compute any
            # windowed metrics.
            if min_rtt_us != -1:
                # Move the window start indices later in time. The min RTT
                # estimate will never increase, so we do not need to investigate
                # whether the start of the window moved earlier in time.
                for win in features.WINDOWS:
                    win_state[win]["window_start_idx"] = utils.find_bound(
                        output[features.ARRIVAL_TIME_FET],
                        target=recv_time_cur_us - (win * min_rtt_us),
                        min_idx=win_state[win]["window_start_idx"],
                        max_idx=j,
                        which="after")

            # Windowed metrics.
            for (metric, _), win in itertools.product(features.WINDOWED,
                                                      features.WINDOWS):
                # If we cannot estimate the min RTT, then we cannot compute any
                # windowed metrics.
                if skip_smoothed or min_rtt_us == -1:
                    continue

                # Calculate windowed metrics only if an entire window has
                # elapsed since the start of the flow.
                win_size_us = win * min_rtt_us
                if recv_time_cur_us - first_data_time_us < win_size_us:
                    continue

                # A window requires at least two packets. Note that this means
                # the the first packet will always be skipped.
                win_start_idx = win_state[win]["window_start_idx"]
                if win_start_idx == j:
                    continue

                metric = features.make_win_metric(metric, win)
                if metric.startswith(features.INTERARR_TIME_FET):
                    new = utils.safe_div(
                        utils.safe_sub(
                            recv_time_cur_us,
                            output[win_start_idx][features.ARRIVAL_TIME_FET]),
                        j - win_start_idx)
                elif metric.startswith(features.INV_INTERARR_TIME_FET):
                    new = utils.safe_mul(
                        8 * 1e6 * wirelen_B,
                        utils.safe_div(
                            1, output[j][features.make_win_metric(
                                features.INTERARR_TIME_FET, win)]))
                elif metric.startswith(features.TPUT_FET):
                    # Treat the first packet in the window as the beginning of
                    # time. Calculate the average throughput over all but the
                    # first packet.
                    #
                    # Sum up the payloads of the packets in the window.
                    total_bytes = utils.safe_sum(output[features.WIRELEN_FET],
                                                 start_idx=win_start_idx + 1,
                                                 end_idx=j)
                    # Divide by the duration of the window.
                    start_time_us = (
                        output[win_start_idx][features.ARRIVAL_TIME_FET]
                        if win_start_idx >= 0 else -1)
                    end_time_us = output[j][features.ARRIVAL_TIME_FET]
                    tput_bps = utils.safe_div(
                        utils.safe_mul(total_bytes, 8),
                        utils.safe_div(
                            utils.safe_sub(end_time_us, start_time_us), 1e6))
                    # If the throughput exceeds the bandwidth, then record a
                    # warning and do not record this throughput.
                    if tput_bps != -1 and tput_bps > exp.bw_bps:
                        win_to_errors[win] += 1
                        continue
                elif metric.startswith(features.TPUT_SHARE_FRAC_FET):
                    # This is calculated at the end.
                    continue
                elif metric.startswith(features.TOTAL_TPUT_FET):
                    # This is calcualted at the end.
                    continue
                elif metric.startswith(features.TPUT_FAIR_SHARE_BPS_FET):
                    # This is calculated at the end.
                    continue
                elif metric.startswith(features.TPUT_TO_FAIR_SHARE_RATIO_FET):
                    # This is calculated at the end.
                    continue
                elif metric.startswith(features.RTT_FET):
                    new = utils.safe_mean(output[features.RTT_FET],
                                          win_start_idx, j)
                elif metric.startswith(features.RTT_RATIO_FET):
                    new = utils.safe_mean(output[features.RTT_RATIO_FET],
                                          win_start_idx, j)
                elif metric.startswith(features.LOSS_EVENT_RATE_FET):
                    rtt_us = output[j][features.make_win_metric(
                        features.RTT_FET, win)]
                    if rtt_us == -1:
                        # The RTT estimate is -1 (unknown), so we
                        # cannot compute the loss event rate.
                        continue

                    cur_start_idx = win_state[win][
                        "current_loss_event_start_idx"]
                    cur_start_time = win_state[win][
                        "current_loss_event_start_time"]
                    if pkt_loss_cur_estimate > 0:
                        # There was a loss since the last packet.
                        #
                        # The index of the first packet in the current
                        # loss event.
                        new_start_idx = (j + pkt_loss_total_estimate -
                                         pkt_loss_cur_estimate)

                        if cur_start_idx == 0:
                            # This is the first loss event.
                            #
                            # Naive fix for the loss event rate
                            # calculation The described method in the
                            # RFC is complicated for the first event
                            # handling.
                            cur_start_idx = 1
                            cur_start_time = 0
                            new = 1 / j
                        else:
                            # This is not the first loss event. See if
                            # any of the newly-lost packets start a
                            # new loss event.
                            #
                            # The average time between when packets
                            # should have arrived, since we received
                            # the last packet.
                            loss_interval = (
                                (recv_time_cur_us - recv_time_prev_us) /
                                (pkt_loss_cur_estimate + 1))

                            # Look at each lost packet...
                            for k in range(pkt_loss_cur_estimate):
                                # Compute the approximate time at
                                # which the packet should have been
                                # received if it had not been lost.
                                loss_time = (recv_time_prev_us +
                                             (k + 1) * loss_interval)

                                # If the time of this loss is more
                                # than one RTT from the time of the
                                # start of the current loss event,
                                # then this is a new loss event.
                                if loss_time - cur_start_time >= rtt_us:
                                    # Record the number of packets
                                    # between the start of the new
                                    # loss event and the start of the
                                    # previous loss event.
                                    win_state[win][
                                        "loss_event_intervals"].appendleft(
                                            new_start_idx - cur_start_idx)
                                    # Potentially discard an old event.
                                    if len(win_state[win]
                                           ["loss_event_intervals"]) > win:
                                        win_state[win][
                                            "loss_event_intervals"].pop()

                                    cur_start_idx = new_start_idx
                                    cur_start_time = loss_time
                                # Calculate the index at which the
                                # new loss event begins.
                                new_start_idx += 1

                            new = compute_weighted_average(
                                (j + pkt_loss_total_estimate - cur_start_idx),
                                win_state[win]["loss_event_intervals"],
                                win_state[win]["loss_interval_weights"])
                    elif pkt_loss_total_estimate > 0:
                        # There have been no losses since the last
                        # packet, but the total loss is nonzero.
                        # Increase the size of the current loss event.
                        new = compute_weighted_average(
                            j + pkt_loss_total_estimate - cur_start_idx,
                            win_state[win]["loss_event_intervals"],
                            win_state[win]["loss_interval_weights"])
                    else:
                        # There have never been any losses, so the
                        # loss event rate is 0.
                        new = 0

                    # Record the new values of the state variables.
                    win_state[win][
                        "current_loss_event_start_idx"] = cur_start_idx
                    win_state[win][
                        "current_loss_event_start_time"] = cur_start_time
                elif metric.startswith(features.SQRT_LOSS_EVENT_RATE_FET):
                    # Use the loss event rate to compute
                    # 1 / sqrt(loss event rate).
                    new = utils.safe_div(
                        1,
                        utils.safe_sqrt(output[j][features.make_win_metric(
                            features.LOSS_EVENT_RATE_FET, win)]))
                elif metric.startswith(features.LOSS_RATE_FET):
                    win_losses = utils.safe_sum(
                        output[features.PACKETS_LOST_FET], win_start_idx + 1,
                        j)
                    new = utils.safe_div(win_losses,
                                         win_losses + (j - win_start_idx))
                elif metric.startswith(features.MATHIS_TPUT_FET):
                    # tput = (MSS / RTT) * (C / sqrt(p))
                    new = utils.safe_mul(
                        utils.safe_div(
                            utils.safe_mul(8, output[j][features.PAYLOAD_FET]),
                            utils.safe_div(output[j][features.RTT_FET], 1e6)),
                        utils.safe_div(
                            MATHIS_C,
                            utils.safe_sqrt(output[j][features.make_win_metric(
                                features.LOSS_EVENT_RATE_FET, win)])))
                else:
                    raise Exception(f"Unknown windowed metric: {metric}")
                output[j][metric] = new

            prev_seq = recv_seq
            prev_payload_B = payload_B
            highest_seq = (prev_seq if highest_seq is None else max(
                highest_seq, prev_seq))
            # In the event of sequence number wraparound, reset the sequence
            # number tracking.
            #
            # TODO: Test sequence number wraparound logic.
            if (recv_seq != -1
                    and recv_seq + (1 if packet_seq else payload_B) > 2**32):
                print(
                    "Warning: Sequence number wraparound detected for packet "
                    f"{j} of flow {flw} in: {exp_flp}")
                highest_seq = None
                prev_seq = None

        # Get the sequence number of the last received packet.
        last_seq = output[-1][features.SEQ_FET]
        if last_seq == -1:
            print("Warning: Unable to calculate retransmission or bottleneck "
                  "queue drop rates due to unknown last sequence number for "
                  f"(UDP?) flow {flw_idx} in: {exp_flp}")
        else:
            # Calculate the true number of retransmissions using the sender
            # traces.
            #
            # Truncate the sent packets at the last occurence of the last packet to
            # be received.
            #
            # Find when the last received packet was sent. Assume that if this
            # packet was retransmitted, then the last retransmission is the one
            # that arrived at the receiver (which may be an incorrect
            # assumption).
            snd_idx = len(snd_data_pkts) - 1
            while snd_idx >= 0:
                if snd_data_pkts[snd_idx][features.SEQ_FET] == last_seq:
                    # unique_snd_pkts, counts = np.unique(
                    #     snd_data_pkts[:snd_idx + 1][features.SEQ_FET],
                    #     return_counts=True)
                    # unique_snd_pkts = unique_snd_pkts.tolist()
                    # counts = counts.tolist()
                    # all_retrans = [
                    #     (seq, counts)
                    #     for seq, counts in zip(unique_snd_pkts, counts)
                    #     if counts > 1]

                    # tot_pkts = snd_idx + 1

                    # The retransmission rate is:
                    #     1 - unique packets / total packets.
                    output[-1][features.RETRANS_RATE_FET] = (
                        1 -
                        # Find the number of unique sequence numbers, from the
                        # beginning up until when the last received packet was
                        # sent.
                        np.unique(snd_data_pkts[:snd_idx + 1][features.SEQ_FET]
                                  ).shape[0] /
                        # Convert from index to packet count.
                        (snd_idx + 1))
                    break
                snd_idx -= 1
            else:
                print("Warning: Did not find when the last received packet "
                      f"(seq: {last_seq}) was sent for flow {flw_idx} in: "
                      f"{exp_flp}")

            # Calculate the true drop rate at the bottleneck queue using the
            # bottleneck queue logs.
            client_port = flw[0]
            deq_idx = None
            drop_rate = None
            if q_log is None:
                print(
                    f"Warning: Unable to find bottleneck queue log: {q_log_flp}"
                )
            else:
                # Find the dequeue log corresponding to the last packet that was
                # received.
                for record_idx, record in reversed(q_log):
                    if (record[0] == "deq" and record[2] == client_port
                            and record[3] == last_seq):
                        deq_idx = record_idx
                        break
            if deq_idx is None:
                print("Warning: Did not find when the last received packet "
                      f"(seq: {last_seq}) was dequeued for flow {flw_idx} in: "
                      f"{exp_flp}")
            else:
                # Find the most recent stats log before the last received
                # packet was dequeued.
                for _, record in reversed(q_log[:deq_idx]):
                    if record[0] == "stats" and record[1] == client_port:
                        drop_rate = record[4] / (record[2] + record[4])
                        break
            if drop_rate is None:
                print(
                    "Warning: Did not calculate the drop rate at the bottleneck "
                    f"queue for flow {flw_idx} in: {exp_flp}")
            else:
                output[-1][features.DROP_RATE_FET] = drop_rate

        # Make sure that all output rows were used.
        used_rows = np.sum(output[features.ARRIVAL_TIME_FET] != -1)
        total_rows = output.shape[0]
        assert used_rows == total_rows, \
            (f"Error: Used only {used_rows} of {total_rows} rows for flow "
             f"{flw_idx} in: {exp_flp}")

        flw_results[flw] = output

    # Save memory by explicitly deleting the sent and received packets
    # after they have been parsed. This happens outside of the above
    # for-loop because only the last iteration's packets are not
    # automatically cleaned up by now (they go out of scope when the
    # *_pkts variables are overwritten by the next loop).
    del snd_data_pkts
    del recv_data_pkts
    del recv_ack_pkts

    if not skip_smoothed:
        # Maps window the index of the packet at the start of that window.
        win_to_start_idx = {win: 0 for win in features.WINDOWS}

        # Merge the flow data into a unified timeline.
        combined = []
        for flw in flws:
            num_pkts = flw_results[flw].shape[0]
            merged = np.empty((num_pkts, ),
                              dtype=[(features.WIRELEN_FET, "int32"),
                                     (features.MIN_RTT_FET, "int32"),
                                     ("client port", "int32"),
                                     ("server port", "int32"),
                                     ("index", "int32")])
            merged[features.WIRELEN_FET] = flw_results[flw][
                features.WIRELEN_FET]
            merged[features.MIN_RTT_FET] = flw_results[flw][
                features.MIN_RTT_FET]
            merged["client port"].fill(flw[0])
            merged["server port"].fill(flw[1])
            merged["index"] = np.arange(num_pkts)
            combined.append(merged)
        zipped_arr_times, zipped_dat = utils.zip_timeseries(
            [flw_results[flw][features.ARRIVAL_TIME_FET] for flw in flws],
            combined)

        for j in range(zipped_arr_times.shape[0]):
            min_rtt_us = zipped_dat[j][features.MIN_RTT_FET]
            if min_rtt_us == -1:
                continue

            for win in features.WINDOWS:
                # The bounds should never go backwards, so start the
                # search at the current bound.
                win_to_start_idx[win] = utils.find_bound(
                    zipped_arr_times,
                    target=(zipped_arr_times[j] -
                            (win * zipped_dat[j][features.MIN_RTT_FET])),
                    min_idx=win_to_start_idx[win],
                    max_idx=j,
                    which="after")
                # If the window's trailing edge caught up with its
                # leading edge, then skip this flow.
                if win_to_start_idx[win] >= j:
                    continue

                total_tput_bps = utils.safe_div(
                    utils.safe_mul(
                        # Accumulate the bytes received by this flow during this
                        # window. When calculating the average throughput, we
                        # must exclude the first packet in the window.
                        utils.safe_sum(zipped_dat[features.WIRELEN_FET],
                                       start_idx=win_to_start_idx[win] + 1,
                                       end_idx=j),
                        8 * 1e6),
                    utils.safe_sub(zipped_arr_times[j],
                                   zipped_arr_times[win_to_start_idx[win]]))
                # Check if this throughput is erroneous.
                if total_tput_bps > exp.bw_bps:
                    win_to_errors[win] += 1
                else:
                    # Extract the flow to which this packet belongs, as well as
                    # its index in its flow.
                    flw = tuple(zipped_dat[j][["client port",
                                               "server port"]].tolist())
                    index = zipped_dat[j]["index"]
                    flw_results[flw][index][features.make_win_metric(
                        features.TOTAL_TPUT_FET, win)] = total_tput_bps
                    # Use the total throughput and the number of active flows to
                    # calculate the throughput fair share.
                    flw_results[flw][index][features.make_win_metric(
                        features.TPUT_FAIR_SHARE_BPS_FET,
                        win)] = (utils.safe_div(
                            total_tput_bps,
                            flw_results[flw][index][features.ACTIVE_FLOWS_FET])
                                 )
                    # Divide the flow's throughput by the total throughput.
                    tput_share = utils.safe_div(
                        flw_results[flw][index][features.make_win_metric(
                            features.TPUT_FET, win)], total_tput_bps)
                    flw_results[flw][index][features.make_win_metric(
                        features.TPUT_SHARE_FRAC_FET, win)] = tput_share
                    # Calculate the ratio of tput share to bandwidth fair share.
                    flw_results[flw][index][features.make_win_metric(
                        features.TPUT_TO_FAIR_SHARE_RATIO_FET,
                        win)] = (utils.safe_div(
                            tput_share, flw_results[flw][index][
                                features.BW_FAIR_SHARE_FRAC_FET]))

    print(f"\tFinal window durations in: {exp_flp}:")
    for win in features.WINDOWS:
        print(
            f"\t\t{win}:",
            ", ".join(f"{dur_us} us" if dur_us > 0 else "unknown"
                      for dur_us in (win * np.asarray([
                          res[-1][features.MIN_RTT_FET]
                          for res in flw_results.values()
                      ])).tolist()))
    print(f"\tWindow errors in: {exp_flp}")
    for win in features.WINDOWS:
        print(f"\t\t{win}:", win_to_errors[win])
    smallest_safe_win = 0
    for win in sorted(features.WINDOWS):
        if win_to_errors[win] == 0:
            print(f"\tSmallest safe window size is {win} in: {exp_flp}")
            smallest_safe_win = win
            break
    else:
        print(f"Warning: No safe window sizes in: {exp_flp}")

    # Determine if there are any NaNs or Infs in the results. For the results
    # for each flow, look through all features (columns) and make a note of the
    # features that bad values. Flatten these lists of feature names, using a
    # set comprehension to remove duplicates.
    bad_fets = {
        fet
        for flw_dat in flw_results.values() for fet in flw_dat.dtype.names
        if not np.isfinite(flw_dat[fet]).all()
    }
    if bad_fets:
        print(f"Warning: Experiment {exp_flp} has NaNs of Infs in features: "
              f"{bad_fets}")

    # Save the results.
    if path.exists(out_flp):
        print(f"\tOutput already exists: {out_flp}")
    else:
        print(f"\tSaving: {out_flp}")
        np.savez_compressed(
            out_flp, **{
                str(k + 1): v
                for k, v in enumerate(flw_results[flw] for flw in flws)
            })

    return smallest_safe_win
示例#3
0
    def learn_from_buffer(self, session):
        try:  # fetch as many trajectories as possible from queue and add to experience buffer
            while True:
                trajectory = self.queue.get(block=True, timeout=0.05)
                self.returns.append(trajectory[-1])
                self.buffer.add_trajectory(trajectory)
        except Empty:
            # start training Loop
            if self.buffer.filled:
                prev_gstep = session.run(self.model.global_step)
                start_time = time.time()

                # Epoch training
                values = np.zeros(5)
                steps_per_epoch = 100
                for step in range(steps_per_epoch):
                    trajectory = self.buffer.sample_trajectories()
                    obs, actions, rewards, dones, behavior_action_probs, bootstrap_state = trajectory

                    B, H, *obs_shape = obs.shape  # (batch_size, horizon)
                    normed_flat_obs, _ = self.normalizer.normalize(
                        obs, rewards, update_internal_with_session=session)

                    # determine π(a|s) for each state in trajectory batch  # TODO changed
                    # _policy_action_probs, flat_values = self.model.get_policy_probs_and_values(session=session,
                    #                                                                            observation=normed_flat_obs)
                    _policy_action_probs, flat_values = self.model.get_policy_probs_and_values(
                        session=session,
                        observation=obs.reshape((B * H, *obs_shape)))
                    # normed_bootstrap_state, normed_rews = self.normalizer.normalize(bootstrap_state, rewards)
                    _policy_action_probs = _policy_action_probs.reshape(
                        (B, H, -1))
                    bootstrap_value = session.run(
                        self.model.values,
                        # feed_dict={self.model.observations: normed_bootstrap_state})  TODO changed
                        feed_dict={self.model.observations: bootstrap_state})

                    policy_action_probs = np.zeros(shape=(B, H),
                                                   dtype=np.float32)

                    for b in range(
                            B
                    ):  # get π(a|s) for each (s, a)-pair in trajectory batch
                        for h in range(H):
                            policy_action_probs[b, h] = _policy_action_probs[
                                b, h, actions[b, h]]

                    # print('np.max =', np.max(policy_action_probs))
                    # print('np.min =', np.min(policy_action_probs))

                    v_trace = calculate_v_trace(
                        policy_action_probs=policy_action_probs,
                        values=flat_values.reshape((B, H)),
                        rewards=rewards.reshape((B, H)),  # TODO changed
                        bootstrap_value=bootstrap_value,
                        behavior_action_probs=behavior_action_probs,
                        rho_bar=self.rho_clip)
                    # normed_adv = U.normalize(v_trace.policy_adv.flatten())
                    feed = {
                        self.model.observations:
                        normed_flat_obs,
                        self.model.v_trace_targets:
                        v_trace.v_s.flatten(),
                        self.model.q_targets:
                        v_trace.policy_adv.flatten(),
                        self.model.actions:
                        actions.flatten(),
                        self.model.behavior_policy_probs:
                        behavior_action_probs.flatten()
                    }
                    trained_ops = session.run(self.model.train_ops,
                                              feed_dict=feed)
                    _, gstep, loss, value, policy, entropy_loss, entropy = trained_ops
                    values += np.array(
                        [loss, value, policy, entropy_loss, entropy])

                values /= steps_per_epoch
                summary = {
                    'Debug/rho_mean':
                    np.mean(v_trace.clipped_rho),
                    'Debug/rho_std':
                    np.std(v_trace.clipped_rho),
                    'Debug/rho_min':
                    np.min(v_trace.clipped_rho),
                    'Debug/rho_max':
                    np.max(v_trace.clipped_rho),
                    'Debug/policy_adv_mean':
                    np.mean(v_trace.policy_adv),
                    'Debug/policy_adv_std':
                    np.std(v_trace.policy_adv),
                    'Debug/policy_adv_min':
                    np.min(v_trace.policy_adv),
                    'Debug/policy_adv_max':
                    np.max(v_trace.policy_adv),
                    'Training/Loss':
                    values[0],
                    'Training/Value Loss':
                    values[1],
                    'Training/Policy Loss':
                    values[2],
                    'Training/Entropy Loss':
                    values[3],
                    'Training/Entropy':
                    values[4],
                    'Training/Buffer Fill Level':
                    self.buffer.count,
                    'Training/Gsteps_per_sec':
                    (gstep - prev_gstep) / (time.time() - start_time)
                }
                if self.returns:
                    summary['Training/Episode Return'] = U.safe_mean(
                        self.returns)
                self.logger.write(summary, global_step=gstep)
                print(
                    'Global step {}: Returns={:0.1f}, Entropy={:0.2f}, Value Loss={:0.2f}'
                    .format(gstep, U.safe_mean(self.returns), values[4],
                            values[1]))
                self.returns = []
            else:
                print(
                    'Learner waits until self.buffer is filled ({}/{})'.format(
                        self.buffer.count, self.buffer.min_fill_level))
                time.sleep(5)
示例#4
0
def get_avg_tputs(flp):
    """ Returns the average throughput for each flow. """
    with np.load(flp) as fil:
        return (utils.Exp(flp),
                [utils.safe_mean(fil[flw][TPUT_KEY]) for flw in fil.files])
示例#5
0
def parse_pcap(sim_dir, out_dir):
    """ Parse a PCAP file. """
    print(f"Parsing: {sim_dir}")
    sim = utils.Sim(sim_dir)
    assert sim.unfair_flws > 0, f"No unfair flows to analyze: {sim_dir}"

    # Construct the output filepaths.
    out_flp = path.join(out_dir, f"{sim.name}.npz")
    # If the output file exists, then we do not need to parse this file.
    if path.exists(out_flp):
        print(f"    Already parsed: {sim_dir}")
        return

    # Process PCAP files from unfair senders and receivers.
    #
    # The final output, with one entry per unfair flow.
    unfair_flws = []
    for unfair_idx in range(sim.unfair_flws):
        # Since this will not be used in practice, we can calculate
        # the min one-way delay using the simulation's parameters.
        one_way_us = sim.btl_delay_us + 2 * sim.edge_delays[unfair_idx]

        # Packet lists are of tuples of the form:
        #     (seq, sender, timestamp us, timestamp option)
        sent_pkts = utils.parse_packets(path.join(
            sim_dir, f"{sim.name}-{unfair_idx + 2}-0.pcap"),
                                        sim.payload_B,
                                        direction="data")
        recv_pcap_flp = path.join(
            sim_dir,
            (f"{sim.name}-{unfair_idx + 2 + sim.unfair_flws + sim.fair_flws}-0"
             ".pcap"))
        recv_pkts = utils.parse_packets(recv_pcap_flp,
                                        sim.payload_B,
                                        direction="data")
        # Ack packets for RTT calculation
        ack_pkts = utils.parse_packets(recv_pcap_flp,
                                       sim.payload_B,
                                       direction="ack")

        # State that the windowed metrics need to track across packets.
        win_state = {
            win: {
                # The index at which this window starts.
                "window_start_idx": 0,
                # The "loss event rate".
                "loss_interval_weights": make_interval_weight(8),
                "loss_event_intervals": collections.deque(),
                "current_loss_event_start_idx": 0,
                "current_loss_event_start_time": 0,
                # For "loss rate true".
                "loss_queue_true": collections.deque(),
                # For "loss rate estimated".
                "loss_queue_estimate": collections.deque()
            }
            for win in WINDOWS
        }

        # The final output. -1 implies that a value was unable to be
        # calculated.
        output = np.empty(len(recv_pkts), dtype=DTYPE)
        output.fill(-1)
        # Total number of packet losses up to the current received
        # packet.
        pkt_loss_total_true = 0
        pkt_loss_total_estimate = 0
        # Loss rate estimation.
        prev_pkt_seq = 0
        highest_seq = 0
        # RTT estimation.
        ack_idx = 0

        for j, recv_pkt in enumerate(recv_pkts):
            # Regular metrics.
            recv_pkt_seq = recv_pkt[0]
            output[j]["seq"] = recv_pkt_seq
            recv_time_cur = recv_pkt[2]
            output[j]["arrival time us"] = recv_time_cur

            if j > 0:
                # Receiver-side RTT estimation using the TCP timestamp
                # option. Attempt to find a new RTT estimate. Move
                # ack_idx to the first occurance of the timestamp
                # option TSval corresponding to the current packet's
                # TSecr.
                tsval = ack_pkts[ack_idx][3][0]
                tsecr = recv_pkt[3][1]
                ack_idx_old = ack_idx
                while tsval != tsecr and ack_idx < len(ack_pkts):
                    ack_idx += 1
                    tsval = ack_pkts[ack_idx][3][0]
                if tsval == tsecr:
                    # If we found a timestamp option match, then
                    # update the RTT estimate.
                    rtt_estimate_us = recv_time_cur - ack_pkts[ack_idx][2]
                else:
                    # Otherwise, use the previous RTT estimate and
                    # reset ack_idx to search again for the next
                    # packet.
                    rtt_estimate_us = output[j - 1][make_ewma_metric(
                        "RTT estimate us", alpha=1.)]
                    ack_idx = ack_idx_old
                # Update the min RTT estimate.
                min_rtt_us = utils.safe_min(output[j - 1]["min RTT us"],
                                            rtt_estimate_us)
                output[j]["min RTT us"] = min_rtt_us
                # Compute the new RTT ratio.
                rtt_estimate_ratio = utils.safe_div(rtt_estimate_us,
                                                    min_rtt_us)

                # Calculate the inter-arrival time.
                recv_time_prev = recv_pkts[j - 1][2]
                interarr_time_us = recv_time_cur - recv_time_prev
            else:
                rtt_estimate_us = -1
                rtt_estimate_ratio = -1
                min_rtt_us = -1
                recv_time_prev = -1
                interarr_time_us = -1

            # Calculate the true packet loss rate. Count the number of
            # dropped packets by checking if the sequence numbers at
            # sender and receiver are the same. If not, the packet is
            # dropped, and the pkt_loss_total_true counter increases
            # by one to keep the index offset at sender
            sent_pkt_seq = sent_pkts[j + pkt_loss_total_true][0]
            pkt_loss_total_true_prev = pkt_loss_total_true
            while sent_pkt_seq != recv_pkt_seq:
                # Packet loss
                pkt_loss_total_true += 1
                sent_pkt_seq = sent_pkts[j + pkt_loss_total_true][0]
            # Calculate how many packets were lost since receiving the
            # last packet.
            pkt_loss_cur_true = pkt_loss_total_true - pkt_loss_total_true_prev

            # Receiver-side loss rate estimation. Estimate the losses
            # since the last packet.
            pkt_loss_cur_estimate = math.ceil(
                0 if recv_pkt_seq == prev_pkt_seq + sim.payload_B else ((
                    (recv_pkt_seq - highest_seq - sim.payload_B) /
                    sim.payload_B
                ) if recv_pkt_seq > highest_seq + sim.payload_B else (1 if (
                    recv_pkt_seq < prev_pkt_seq and prev_pkt_seq != highest_seq
                ) else 0)))
            pkt_loss_total_estimate += pkt_loss_cur_estimate
            prev_pkt_seq = recv_pkt_seq
            highest_seq = max(highest_seq, prev_pkt_seq)

            # Calculate the true RTT and RTT ratio. Look up the send
            # time of this packet to calculate the true
            # sender-receiver delay. Assume that, on the reverse path,
            # packets will experience no queuing delay.
            rtt_true_us = (recv_time_cur -
                           sent_pkts[j + pkt_loss_total_true][2] + one_way_us)
            rtt_true_ratio = rtt_true_us / (2 * one_way_us)

            # EWMA metrics.
            for (metric, _), alpha in itertools.product(EWMAS, ALPHAS):
                metric = make_ewma_metric(metric, alpha)
                if "interarrival time us" in metric:
                    new = interarr_time_us
                elif ("throughput p/s" in metric
                      and "mathis model" not in metric):
                    # Do not use the existing interarrival EWMA to
                    # calculate the throughput. Instead, use the true
                    # interarrival time so that the value used to
                    # update the throughput EWMA is not "EWMA-ified"
                    # twice. Divide by 1e6 to convert from
                    # microseconds to seconds.
                    new = utils.safe_div(1,
                                         utils.safe_div(interarr_time_us, 1e6))
                elif "RTT estimate us" in metric:
                    new = rtt_estimate_us
                elif "RTT estimate ratio" in metric:
                    new = rtt_estimate_ratio
                elif "RTT true us" in metric:
                    new = rtt_true_us
                elif "RTT true ratio" in metric:
                    new = rtt_true_ratio
                elif "loss rate estimate" in metric:
                    # See comment in case for "loss rate true".
                    new = pkt_loss_cur_estimate / (pkt_loss_cur_estimate + 1)
                elif "loss rate true" in metric:
                    # Divide the pkt_loss_cur_true by
                    # (pkt_loss_cur_true + 1) because over the course
                    # of sending (pkt_loss_cur_true + 1) packets, one
                    # got through and pkt_loss_cur_true were lost.
                    new = pkt_loss_cur_true / (pkt_loss_cur_true + 1)
                elif "queue occupancy" in metric:
                    # Queue occupancy is calculated using the router
                    # logs, below.
                    continue
                elif "mathis model throughput p/s" in metric:
                    # Use the estimated loss rate to compute the
                    # Mathis model fair throughput. Contrary to the
                    # decision for interarrival time, above, here we
                    # use the value of another EWMA (loss rate
                    # estimate) to compute the new value for the
                    # Mathis model throughput EWMA. I believe that
                    # this is desirable because we want to see how the
                    # metric as a whole reacts to a certain degree of
                    # memory.
                    loss_rate_estimate = (pkt_loss_total_estimate /
                                          j if j > 0 else -1)
                    # Use "safe" operations in case any of the
                    # supporting values are -1 (unknown).
                    new = (-1 if loss_rate_estimate <= 0 else utils.safe_div(
                        MATHIS_C,
                        utils.safe_div(
                            utils.safe_mul(
                                min_rtt_us,
                                utils.safe_sqrt(loss_rate_estimate)), 1e6)))
                elif "mathis model label" in metric:
                    # Use the current throughput and the Mathis model
                    # fair throughput to compute the Mathis model
                    # label.
                    output[j][metric] = utils.safe_mathis_label(
                        output[j][make_ewma_metric("throughput p/s", alpha)],
                        output[j][make_ewma_metric(
                            "mathis model throughput p/s", alpha)])
                    # Continue because the value of this metric is not
                    # an EWMA.
                    continue
                else:
                    raise Exception(f"Unknown EWMA metric: {metric}")
                # Update the EWMA.
                output[j][metric] = utils.safe_update_ewma(
                    -1 if j == 0 else output[j - 1][metric], new, alpha)

            # Windowed metrics.
            for (metric, _), win in itertools.product(WINDOWED, WINDOWS):
                metric = make_win_metric(metric, win)
                # If we have not been able to estimate the min RTT
                # yet, then we cannot compute any of the windowed
                # metrics.
                if min_rtt_us == -1:
                    continue
                win_size_us = win * min_rtt_us

                # Move the start of the window forward.
                while ((recv_time_cur -
                        recv_pkts[win_state[win]["window_start_idx"]][2]) >
                       win_size_us):
                    win_state[win]["window_start_idx"] += 1
                win_start_idx = win_state[win]["window_start_idx"]

                if "average interarrival time us" in metric:
                    new = ((recv_time_cur - recv_pkts[win_start_idx][2]) /
                           (j - win_start_idx + 1))
                elif "average throughput p/s" in metric:
                    # We base the throughput calculation on the
                    # average interarrival time over the window.
                    avg_interarr_time_us = output[j][make_win_metric(
                        "average interarrival time us", win)]
                    # Divide by 1e6 to convert from microseconds to
                    # seconds.
                    new = utils.safe_div(
                        1, utils.safe_div(avg_interarr_time_us, 1e6))
                elif "average RTT estimate us" in metric:
                    new = utils.safe_mean(
                        output[make_ewma_metric("RTT estimate us", alpha=1.)],
                        win_start_idx, j)
                elif "average RTT estimate ratio" in metric:
                    new = utils.safe_mean(
                        output[make_ewma_metric("RTT estimate ratio",
                                                alpha=1.)], win_start_idx, j)
                elif "average RTT true us" in metric:
                    new = utils.safe_mean(
                        output[make_ewma_metric("RTT true us", alpha=1.)],
                        win_start_idx, j)
                elif "average RTT true ratio" in metric:
                    new = utils.safe_mean(
                        output[make_ewma_metric("RTT true ratio", alpha=1.)],
                        win_start_idx, j)
                elif "loss event rate" in metric and "1/sqrt" not in metric:
                    rtt_estimate_us = output[j][make_win_metric(
                        "average RTT estimate us", win)]
                    if rtt_estimate_us == -1:
                        # The RTT estimate is -1 (unknown), so we
                        # cannot compute the loss event rate.
                        continue

                    cur_start_idx = win_state[win][
                        "current_loss_event_start_idx"]
                    cur_start_time = win_state[win][
                        "current_loss_event_start_time"]
                    if pkt_loss_cur_estimate > 0:
                        # There was a loss since the last packet.
                        #
                        # The index of the first packet in the current
                        # loss event.
                        new_start_idx = (j + pkt_loss_total_estimate -
                                         pkt_loss_cur_estimate)

                        if cur_start_idx == 0:
                            # This is the first loss event.
                            #
                            # Naive fix for the loss event rate
                            # calculation The described method in the
                            # RFC is complicated for the first event
                            # handling.
                            cur_start_idx = 1
                            cur_start_time = 0
                            new = 1 / j
                        else:
                            # This is not the first loss event. See if
                            # any of the newly-lost packets start a
                            # new loss event.
                            #
                            # The average time between when packets
                            # should have arrived, since we received
                            # the last packet.
                            loss_interval = ((recv_time_cur - recv_time_prev) /
                                             (pkt_loss_cur_estimate + 1))

                            # Look at each lost packet...
                            for k in range(pkt_loss_cur_estimate):
                                # Compute the approximate time at
                                # which the packet should have been
                                # received if it had not been lost.
                                loss_time = (recv_time_prev +
                                             (k + 1) * loss_interval)

                                # If the time of this loss is more
                                # than one RTT from the time of the
                                # start of the current loss event,
                                # then this is a new loss event.
                                if (loss_time - cur_start_time >=
                                        rtt_estimate_us):
                                    # Record the number of packets
                                    # between the start of the new
                                    # loss event and the start of the
                                    # previous loss event.
                                    win_state[win][
                                        "loss_event_intervals"].appendleft(
                                            new_start_idx - cur_start_idx)
                                    # Potentially discard an old event.
                                    if len(win_state[win]
                                           ["loss_event_intervals"]) > win:
                                        win_state[win][
                                            "loss_event_intervals"].pop()

                                    cur_start_idx = new_start_idx
                                    cur_start_time = loss_time
                                # Calculate the index at which the
                                # new loss event begins.
                                new_start_idx += 1

                            new = compute_weighted_average(
                                (j + pkt_loss_total_estimate - cur_start_idx),
                                win_state[win]["loss_event_intervals"],
                                win_state[win]["loss_interval_weights"])
                    elif pkt_loss_total_estimate > 0:
                        # There have been no losses since the last
                        # packet, but the total loss is nonzero.
                        # Increase the size of the current loss event.
                        new = compute_weighted_average(
                            j + pkt_loss_total_estimate - cur_start_idx,
                            win_state[win]["loss_event_intervals"],
                            win_state[win]["loss_interval_weights"])
                    else:
                        # There have never been any losses, so the
                        # loss event rate is 0.
                        new = 0

                    # Record the new values of the state variables.
                    win_state[win][
                        "current_loss_event_start_idx"] = cur_start_idx
                    win_state[win][
                        "current_loss_event_start_time"] = cur_start_time
                elif "1/sqrt loss event rate" in metric:
                    # Use the loss event rate to compute
                    # 1 / sqrt(loss event rate).
                    new = utils.safe_div(
                        1,
                        utils.safe_sqrt(output[j][make_win_metric(
                            "loss event rate", win)]))
                elif "loss rate estimate" in metric:
                    # We do not need to check whether recv_time_prev
                    # is -1 (unknown) because the windowed metrics
                    # skip the case where j == 0.
                    win_state[win]["loss_queue_estimate"], new = loss_rate(
                        win_state[win]["loss_queue_estimate"], win_start_idx,
                        pkt_loss_cur_estimate, recv_time_cur, recv_time_prev,
                        win_size_us, j)
                elif "loss rate true" in metric:
                    # We do not need to check whether recv_time_prev
                    # is -1 (unknown) because the windowed metrics
                    # skip the case where j == 0.
                    win_state[win]["loss_queue_true"], new = loss_rate(
                        win_state[win]["loss_queue_true"], win_start_idx,
                        pkt_loss_cur_true, recv_time_cur, recv_time_prev,
                        win_size_us, j)
                elif "queue occupancy" in metric:
                    # Queue occupancy is calculated using the router
                    # logs, below.
                    continue
                elif "mathis model throughput p/s" in metric:
                    # Use the loss event rate to compute the Mathis
                    # model fair throughput.
                    loss_rate_estimate = (pkt_loss_total_estimate /
                                          j if j > 0 else -1)
                    new = utils.safe_div(
                        MATHIS_C,
                        utils.safe_div(
                            utils.safe_mul(
                                min_rtt_us,
                                utils.safe_sqrt(loss_rate_estimate)), 1e6))
                elif "mathis model label" in metric:
                    # Use the current throughput and Mathis model
                    # fair throughput to compute the Mathis model
                    # label.
                    new = utils.safe_mathis_label(
                        output[j][make_win_metric("average throughput p/s",
                                                  win)],
                        output[j][make_win_metric(
                            "mathis model throughput p/s", win)])
                else:
                    raise Exception(f"Unknown windowed metric: {metric}")
                output[j][metric] = new
        unfair_flws.append(output)

    # Save memory by explicitly deleting the sent and received packets
    # after they have been parsed. This happens outside of the above
    # for-loop because only the last iteration's sent and received
    # packets are not automatically cleaned up by now (they go out of
    # scope when the sent_pkts and recv_pkts variables are overwritten
    # by the next loop).
    del sent_pkts
    del recv_pkts

    # Process pcap files from the bottleneck router to determine queue
    # occupency. Packet lists are of tuples of the form:
    #     (seq, sender, timestamp us, timestamp option)
    router_pkts = utils.parse_packets(path.join(sim_dir,
                                                f"{sim.name}-1-0.pcap"),
                                      sim.payload_B,
                                      direction="data")
    # State pertaining to each flow.
    flw_state = {
        flw: {
            # Index of the output array where the queue occupency
            # results should be appended.
            "output_idx": 0,
            # The number of other flows' packets that have arrived
            # since the last packet for this flow.
            "packets_since_last": 0,
            # The number of packets from this flow currently in the
            # window.
            "window_flow_packets": {win: 0
                                    for win in WINDOWS}
        }
        for flw in range(sim.unfair_flws)
    }
    # The index of the first packet in the window, for every window
    # size.
    win_start_idxs = {win: 0 for win in WINDOWS}

    # Loop over all of the packets receiver by the bottleneck
    # router. Note that we process all flows at once.
    for j, router_pkt in enumerate(router_pkts):
        _, sender, curr_time, _ = router_pkt
        # Process only packets that are part of one of the unfair
        # flows. Discard packets that did not make it to the receiver
        # (e.g., at the end of the experiment).
        if (sender < sim.unfair_flws and flw_state[sender]["output_idx"] <
                unfair_flws[sender].shape[0]):
            # We cannot move this above the if-statement condition
            # because it is valid only if sender < sim.unfair_flws.
            output_idx = flw_state[sender]["output_idx"]

            # EWMA metrics.
            for (metric, _), alpha in itertools.product(EWMAS, ALPHAS):
                metric = make_ewma_metric(metric, alpha)
                if "interarrival time us" in metric:
                    # The interarrival time is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "throughput p/s" in metric:
                    # The throughput is calculated using the sender
                    # and/or receiver logs, above.
                    continue
                if "RTT estimate us" in metric:
                    # The RTT is calculated using the sender and/or
                    # receiver logs, above.
                    continue
                if "RTT estimate ratio" in metric:
                    # The RTT ratio is calculated using the sender
                    # and/or receiver logs, above.
                    continue
                if "RTT true us" in metric:
                    # The RTT is calculated using the sender and/or
                    # receiver logs, above.
                    continue
                if "RTT true ratio" in metric:
                    # The RTT ratio is calculated using the sender
                    # and/or receiver logs, above.
                    continue
                if "loss rate estimate" in metric:
                    # The estiamted loss rate is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "loss rate true" in metric:
                    # The true loss rate is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "queue occupancy" in metric:
                    # The instanteneous queue occupancy is 1 divided
                    # by the number of packets that have entered the
                    # queue since the last packet from the same
                    # flow. This is the fraction of packets added to
                    # the queue corresponding to this flow, over the
                    # time since when the flow's last packet arrived.
                    new = utils.safe_div(
                        1, flw_state[sender]["packets_since_last"])
                elif "mathis model throughput p/s" in metric:
                    # The Mathis model fair throughput is calculated
                    # using the sender and/or receiver logs, above.
                    continue
                elif "mathis model label" in metric:
                    # The Mathis model label is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                else:
                    raise Exception(f"Unknown EWMA metric: {metric}")
                unfair_flws[sender][output_idx][metric] = (
                    utils.safe_update_ewma(
                        unfair_flws[sender][output_idx - 1][metric], new,
                        alpha))

            # Windowed metrics.
            for (metric, _), win in itertools.product(WINDOWED, WINDOWS):
                metric = make_win_metric(metric, win)
                if "average interarrival time us" in metric:
                    # The average interarrival time is calculated
                    # using the sender and/or receiver logs, above.
                    continue
                if "average throughput p/s" in metric:
                    # The average throughput is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "average RTT estimate us" in metric:
                    # The average RTT is calculated using the sender
                    # and/or receiver logs, above.
                    continue
                if "average RTT estimate ratio" in metric:
                    # The average RTT ratio is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "average RTT true us" in metric:
                    # The average RTT is calculated using the sender
                    # and/or receiver logs, above.
                    continue
                if "average RTT true ratio" in metric:
                    # The average RTT ratio is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "loss event rate" in metric:
                    # The loss event rate is calcualted using the
                    # sender and/or receiver logs, above.
                    continue
                if "1/sqrt loss event rate" in metric:
                    # The reciprocal of the square root of the loss
                    # event rate is calculated using the sender and/or
                    # receiver logs, above.
                    continue
                if "loss rate estimate" in metric:
                    # The estimated loss rate is calcualted using the
                    # sender and/or reciever logs, above.
                    continue
                if "loss rate true" in metric:
                    # The true loss rate is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                if "queue occupancy" in metric:
                    win_start_idx = win_start_idxs[win]
                    # By definition, the window now contains one more
                    # packet from this flow.
                    win_flw_pkts = (
                        flw_state[sender]["window_flow_packets"][win] + 1)

                    # The current length of the window.
                    win_cur_us = curr_time - router_pkts[win_start_idx][2]
                    # Extract the RTT estimate.
                    rtt_estimate_us = unfair_flws[sender][output_idx][
                        make_win_metric("average RTT estimate us", win)]
                    if rtt_estimate_us == -1:
                        # The RTT estimate is -1 (unknown), so we
                        # cannot calculate the size of the window. We
                        # must record the new value of
                        # "window_flow_packets".
                        flw_state[sender]["window_flow_packets"][
                            win] = win_flw_pkts
                        continue

                    # Calculate the target length of the window.
                    win_target_us = win * rtt_estimate_us

                    # If the current window size is greater than the
                    # target window size, then shrink the window.
                    while win_cur_us > win_target_us:
                        # If the packet that will be removed from
                        # the window is from this flow, then we
                        # need to decrease our record of the
                        # number of this flow's packets in the
                        # window by one.
                        if router_pkts[win_start_idx][1] == sender:
                            win_flw_pkts -= 1
                        # Move the start of the window forward.
                        win_start_idx += 1
                        win_cur_us = curr_time - router_pkts[win_start_idx][2]

                    # If the current window size is smaller than the
                    # target window size, then grow the window.
                    while (win_start_idx > 0 and win_cur_us < win_target_us):
                        # Move the start of the window backward.
                        win_start_idx -= 1
                        win_cur_us = curr_time - router_pkts[win_start_idx][2]
                        # If the new packet that was added to the
                        # window is from this flow, then we need
                        # to increase our record of the number of
                        # this flow's packets in the window by
                        # one.
                        if router_pkts[win_start_idx][1] == sender:
                            win_flw_pkts += 1

                    # The queue occupancy is the number of this flow's
                    # packets in the window divided by the total
                    # number of packets in the window.
                    new = win_flw_pkts / (j - win_start_idx + 1)
                    # Record the new values of the state variables.
                    win_start_idxs[win] = win_start_idx
                    flw_state[sender]["window_flow_packets"][
                        win] = win_flw_pkts
                elif "mathis model throughput p/s" in metric:
                    # The Mathis model fair throughput is calculated
                    # using the sender and/or receiver logs, above.
                    continue
                elif "mathis model label" in metric:
                    # The Mathis model label is calculated using the
                    # sender and/or receiver logs, above.
                    continue
                else:
                    raise Exception(f"Unknown windowed metric: {metric}")
                unfair_flws[sender][output_idx][metric] = new
            flw_state[sender]["output_idx"] += 1
            # For the current packet's flow, the number of packets
            # since the last packet in this flow is now 1.
            flw_state[sender]["packets_since_last"] = 1
        # For each unfair flow except the current packet's flow,
        # increment the number of packets since the last packet from
        # that flow.
        for flw in range(sim.unfair_flws):
            if flw != sender:
                flw_state[flw]["packets_since_last"] += 1

    # Determine if there are any NaNs or Infs in the results. For the
    # results for each unfair flow, look through all features
    # (columns) and make a note of the features that bad
    # values. Flatten these lists of feature names, using a set
    # comprehension to remove duplicates.
    bad_fets = {
        fet
        for flw_dat in unfair_flws for fet in flw_dat.dtype.names
        if not np.isfinite(flw_dat[fet]).all()
    }
    if bad_fets:
        print(f"    Simulation {sim_dir} has NaNs of Infs in features: "
              f"{bad_fets}")

    # Save the results.
    if path.exists(out_flp):
        print(f"    Output already exists: {out_flp}")
    else:
        print(f"    Saving: {out_flp}")
        np.savez_compressed(
            out_flp, **{str(k + 1): v
                        for k, v in enumerate(unfair_flws)})