/* * Created on Jul 6, 2007 * Created by Paul Gardner * Copyright (C) 2007 Aelitis, All Rights Reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * AELITIS, SAS au capital de 63.529,40 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. * */ package com.aelitis.azureus.core.speedmanager.impl; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Random; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.DisplayFormatters; import org.gudy.azureus2.core3.util.FileUtil; import org.gudy.azureus2.core3.util.IndentWriter; import org.gudy.azureus2.core3.util.SystemTime; import com.aelitis.azureus.core.speedmanager.SpeedManagerLimitEstimate; import com.aelitis.azureus.core.speedmanager.SpeedManagerPingMapper; import com.aelitis.azureus.core.speedmanager.SpeedManagerPingZone; class SpeedManagerPingMapperImpl implements SpeedManagerPingMapper { static final int VARIANCE_GOOD_VALUE = 50; static final int VARIANCE_BAD_VALUE = 150; static final int VARIANCE_MAX = VARIANCE_BAD_VALUE*10; static final int RTT_BAD_MIN = 350; static final int RTT_BAD_MAX = 500; static final int RTT_MAX = 30*1000; // don't make this too large as we don't start considering capacity decreases until this // is full static final int MAX_BAD_LIMIT_HISTORY = 16; static final int SPEED_DIVISOR = 256; private static final int SPEED_HISTORY_PERIOD = 3*60*1000; // 3 min private static final int SPEED_HISTORY_COUNT = SPEED_HISTORY_PERIOD / SpeedManagerImpl.UPDATE_PERIOD_MILLIS; private SpeedManagerImpl speed_manager; private String name; private boolean variance; private boolean trans; private int ping_count; private pingValue[] pings; private int max_pings; private pingValue prev_ping; private int[] x_speeds = new int[ SPEED_HISTORY_COUNT ]; private int[] y_speeds = new int[ SPEED_HISTORY_COUNT ]; private int speeds_next; private LinkedList regions; private int last_x; private int last_y; private int[] recent_metrics = new int[3]; private int recent_metrics_next; private limitEstimate up_estimate; private limitEstimate down_estimate; private LinkedList last_bad_ups; private LinkedList last_bad_downs; private static final int BAD_PROGRESS_COUNTDOWN = 5; private limitEstimate last_bad_up; private int bad_up_in_progress_count; private limitEstimate last_bad_down; private int bad_down_in_progress_count; private limitEstimate best_good_up; private limitEstimate best_good_down; private limitEstimate up_capacity = getNullLimit(); private limitEstimate down_capacity = getNullLimit(); private File history_file; protected SpeedManagerPingMapperImpl( SpeedManagerImpl _speed_manager, String _name, int _entries , boolean _variance, boolean _transient ) { speed_manager = _speed_manager; name = _name; max_pings = _entries; variance = _variance; trans = _transient; init(); } protected synchronized void init() { pings = new pingValue[max_pings]; ping_count = 0; regions = new LinkedList(); up_estimate = getNullLimit(); down_estimate = getNullLimit(); last_bad_ups = new LinkedList(); last_bad_downs = new LinkedList(); last_bad_up = null; bad_up_in_progress_count = 0; last_bad_down = null; bad_down_in_progress_count = 0; best_good_up = null; best_good_down = null; up_capacity = getNullLimit(); down_capacity = getNullLimit(); prev_ping = null; recent_metrics_next = 0; } protected synchronized void loadHistory( File file ) { try{ if ( history_file != null && history_file.equals( file )){ return; } if ( history_file != null ){ saveHistory(); } history_file = file; init(); if ( history_file.exists()){ // skip key intern to save CPU as there are a lot of keys // and we end up ditching the map after it's processed Map map = FileUtil.readResilientFile( history_file.getParentFile(), history_file.getName(), false, false ); List p = (List)map.get( "pings" ); if ( p != null ){ for (int i=0;i<p.size();i++){ Map m = (Map)p.get(i); int x = ((Long)m.get( "x" )).intValue(); int y = ((Long)m.get( "y" )).intValue(); int metric = ((Long)m.get( "m" )).intValue(); if ( i == 0 ){ last_x = 0; last_y = 0; } if ( variance ){ if ( metric > VARIANCE_MAX ){ metric = VARIANCE_MAX; } }else{ if ( metric > RTT_MAX ){ metric = RTT_MAX; } } addPingSupport( x, y, -1, metric ); } } last_bad_ups = loadLimits( map, "lbus" ); last_bad_downs = loadLimits( map, "lbds" ); if ( last_bad_ups.size() > 0 ){ last_bad_up = (limitEstimate)last_bad_ups.get(last_bad_ups.size()-1); } if ( last_bad_downs.size() > 0 ){ last_bad_down = (limitEstimate)last_bad_downs.get(last_bad_downs.size()-1); } best_good_up = loadLimit((Map)map.get( "bgu" )); best_good_down = loadLimit((Map)map.get( "bgd" )); up_capacity = loadLimit((Map)map.get( "upcap" )); down_capacity = loadLimit((Map)map.get( "downcap" )); log( "Loaded " + ping_count + " entries from " + history_file + ": bad_up=" + getLimitString(last_bad_ups) + ", bad_down=" + getLimitString(last_bad_downs)); }else{ // first time with this ASN - removed auto speed test in 4813 so decided to increase // the initial estimated upload limit to avoid starting out too low setEstimatedUploadCapacityBytesPerSec( 75*1024, SpeedManagerLimitEstimate.TYPE_ESTIMATED ); } prev_ping = null; recent_metrics_next = 0; updateLimitEstimates(); }catch( Throwable e ){ Debug.printStackTrace(e); } } protected synchronized void saveHistory() { try{ if ( history_file == null ){ return; } Map map = new HashMap(); List p = new ArrayList(ping_count); // NOTE: add to this you will need to modify the "reset" method appropriately map.put( "pings", p ); for (int i=0;i<ping_count;i++){ pingValue ping = pings[i]; Map m = new HashMap(); p.add( m ); m.put( "x", new Long(ping.getX())); m.put( "y", new Long(ping.getY())); m.put( "m", new Long(ping.getMetric())); } saveLimits( map, "lbus", last_bad_ups ); saveLimits( map, "lbds", last_bad_downs ); if ( best_good_up != null ){ map.put( "bgu", saveLimit( best_good_up )); } if ( best_good_down != null ){ map.put( "bgd", saveLimit( best_good_down )); } map.put( "upcap", saveLimit( up_capacity )); map.put( "downcap", saveLimit( down_capacity )); FileUtil.writeResilientFile( history_file, map ); log( "Saved " + p.size() + " entries to " + history_file ); }catch( Throwable e ){ Debug.printStackTrace(e); } } protected LinkedList loadLimits( Map map, String name ) { LinkedList result = new LinkedList(); List l = (List)map.get(name); if ( l != null ){ for (int i=0;i<l.size();i++){ Map m = (Map)l.get(i); result. add(loadLimit( m )); } } return( result ); } protected limitEstimate loadLimit( Map m ) { if ( m == null ){ return( getNullLimit()); } int speed = ((Long)m.get( "s" )).intValue(); double metric = Double.parseDouble( new String((byte[])m.get("m"))); int hits = ((Long)m.get( "h" )).intValue(); long when = ((Long)m.get("w")).longValue(); byte[] t_bytes = (byte[])m.get("t"); double type = t_bytes==null?SpeedManagerLimitEstimate.TYPE_ESTIMATED :Double.parseDouble( new String( t_bytes )); return( new limitEstimate( speed, type, metric, hits, when, new int[0][] )); } protected void saveLimits( Map map, String name, List limits ) { List l = new ArrayList(); for (int i=0;i<limits.size();i++){ limitEstimate limit = (limitEstimate)limits.get(i); Map m = saveLimit( limit ); l.add( m ); } map.put( name, l ); } protected Map saveLimit( limitEstimate limit ) { if ( limit == null ){ limit = getNullLimit(); } Map m = new HashMap(); m.put( "s", new Long( limit.getBytesPerSec())); m.put( "m", String.valueOf( limit.getMetricRating())); m.put( "t", String.valueOf( limit.getEstimateType())); m.put( "h", new Long( limit.getHits())); m.put( "w", new Long( limit.getWhen())); return( m ); } public boolean isActive() { return( variance ); } protected limitEstimate getNullLimit() { return( new limitEstimate( 0, SpeedManagerLimitEstimate.TYPE_UNKNOWN, 0, 0, 0, new int[0][] )); } protected String getLimitString( List limits ) { String str = ""; for (int i=0;i<limits.size();i++){ str += (i==0?"":",") + ((limitEstimate)limits.get(i)).getString(); } return( str ); } protected void log( String str ) { if ( speed_manager != null ){ speed_manager.log( str ); } } public String getName() { return( name ); } protected synchronized void addSpeed( int x, int y ) { x = x/SPEED_DIVISOR; y = y/SPEED_DIVISOR; if ( x > 65535 )x = 65535; if ( y > 65535 )y = 65535; addSpeedSupport( x, y ); } protected synchronized void addSpeedSupport( int x, int y ) { x_speeds[speeds_next] = x; y_speeds[speeds_next] = y; speeds_next = (speeds_next+1)%SPEED_HISTORY_COUNT; int min_x = Integer.MAX_VALUE; int min_y = Integer.MAX_VALUE; for (int i=0;i<SPEED_HISTORY_COUNT;i++){ min_x = Math.min( min_x, x_speeds[i] ); min_y = Math.min( min_y, y_speeds[i] ); } min_x *= SPEED_DIVISOR; min_y *= SPEED_DIVISOR; if ( up_capacity.getEstimateType() != SpeedManagerLimitEstimate.TYPE_MANUAL){ if ( min_x > up_capacity.getBytesPerSec()){ up_capacity.setBytesPerSec( min_x ); up_capacity.setMetricRating( 0 ); up_capacity.setEstimateType( SpeedManagerLimitEstimate.TYPE_ESTIMATED); speed_manager.informUpCapChanged(); } } if ( down_capacity.getEstimateType() != SpeedManagerLimitEstimate.TYPE_MANUAL){ if ( min_y > down_capacity.getBytesPerSec()){ down_capacity.setBytesPerSec( min_y ); down_capacity.setMetricRating( 0 ); down_capacity.setEstimateType( SpeedManagerLimitEstimate.TYPE_ESTIMATED); speed_manager.informDownCapChanged(); } } } protected synchronized void addPing( int x, int y, int rtt, boolean re_base ) { x = x/SPEED_DIVISOR; y = y/SPEED_DIVISOR; if ( x > 65535 )x = 65535; if ( y > 65535 )y = 65535; if ( rtt > 65535 )rtt = variance?VARIANCE_MAX:RTT_MAX; if ( rtt == 0 )rtt = 1; // ping time won't refer to current x+y due to latencies, apply to average between // current and previous int average_x = (x + last_x )/2; int average_y = (y + last_y )/2; last_x = x; last_y = y; x = average_x; y = average_y; int metric; if ( variance ){ if ( re_base ){ log( "Re-based variance" ); recent_metrics_next = 0; } recent_metrics[recent_metrics_next++%recent_metrics.length] = rtt; int var_metric = 0; int rtt_metric = 0; if ( recent_metrics_next > 1 ){ int entries = Math.min( recent_metrics_next, recent_metrics.length ); int total = 0; for (int i=0;i<entries;i++){ total += recent_metrics[i]; } int average = total/entries; int total_deviation = 0; for (int i=0;i<entries;i++){ int deviation = recent_metrics[i] - average; total_deviation += deviation * deviation; } // we deliberately don't divide by num samples as this accentuates larger deviations var_metric = (int)Math.sqrt( total_deviation ); // variance is a useful measure. however, under some conditions, in particular high // download speeds, we get elevated ping times with little variance // factor this in if ( entries == recent_metrics.length ){ int total_rtt = 0; for (int i=0;i<entries;i++){ total_rtt += recent_metrics[i]; } int average_rtt = total_rtt / recent_metrics.length; if ( average_rtt >= RTT_BAD_MAX ){ rtt_metric = VARIANCE_BAD_VALUE; }else if ( average_rtt > RTT_BAD_MIN ){ int rtt_diff = RTT_BAD_MAX - RTT_BAD_MIN; int rtt_base = average_rtt - RTT_BAD_MIN; rtt_metric = VARIANCE_GOOD_VALUE + (( VARIANCE_BAD_VALUE - VARIANCE_GOOD_VALUE ) * rtt_base ) / rtt_diff; } } } metric = Math.max( var_metric, rtt_metric ); if ( metric < VARIANCE_BAD_VALUE ){ addSpeedSupport( x, y ); }else{ addSpeedSupport( 0, 0 ); } }else{ metric = rtt; } region new_region = addPingSupport( x, y, rtt, metric ); updateLimitEstimates(); if ( variance ){ String up_e = getShortString( getEstimatedUploadLimit( false )) + "," + getShortString(getEstimatedUploadLimit( true )) + "," + getShortString(getEstimatedUploadCapacityBytesPerSec()); String down_e = getShortString(getEstimatedDownloadLimit( false )) + "," + getShortString(getEstimatedDownloadLimit( true )) + "," + getShortString(getEstimatedDownloadCapacityBytesPerSec()); log( "Ping: rtt="+rtt+",x="+x+",y="+y+",m="+metric + (new_region==null?"":(",region=" + new_region.getString())) + ",mr=" + getCurrentMetricRating() + ",up=[" + up_e + (best_good_up==null?"":(":"+getShortString(best_good_up))) + "],down=[" + down_e + (best_good_down==null?"":(":"+getShortString(best_good_down))) + "]" + ",bu="+getLimitStr(last_bad_ups,true)+",bd="+getLimitStr(last_bad_downs,true)); } } protected region addPingSupport( int x, int y, int rtt, int metric ) { if ( ping_count == pings.length ){ // discard oldest pings and reset int to_discard = pings.length/10; if ( to_discard < 3 ){ to_discard = 3; } ping_count = pings.length - to_discard; System.arraycopy(pings, to_discard, pings, 0, ping_count); for (int i=0;i<to_discard;i++ ){ regions.removeFirst(); } } pingValue ping = new pingValue( x, y, metric ); pings[ping_count++] = ping; region new_region = null; if ( prev_ping != null ){ new_region = new region(prev_ping,ping); regions.add( new_region ); } prev_ping = ping; return( new_region ); } public synchronized int[][] getHistory() { int[][] result = new int[ping_count][]; for (int i=0;i<ping_count;i++){ pingValue ping = pings[i]; result[i] = new int[]{ SPEED_DIVISOR*ping.getX(), SPEED_DIVISOR*ping.getY(), ping.getMetric()}; } return( result ); } public synchronized SpeedManagerPingZone[] getZones() { return((SpeedManagerPingZone[])regions.toArray( new SpeedManagerPingZone[regions.size()] )); } public synchronized SpeedManagerLimitEstimate getEstimatedUploadLimit( boolean persistent ) { return( adjustForPersistence( up_estimate, best_good_up, last_bad_up, persistent )); } public synchronized SpeedManagerLimitEstimate getEstimatedDownloadLimit( boolean persistent ) { return( adjustForPersistence( down_estimate, best_good_down, last_bad_down, persistent )); } public SpeedManagerLimitEstimate getLastBadUploadLimit() { return( last_bad_up ); } public SpeedManagerLimitEstimate getLastBadDownloadLimit() { return( last_bad_down ); } public synchronized SpeedManagerLimitEstimate[] getBadUploadHistory() { return((SpeedManagerLimitEstimate[])last_bad_ups.toArray(new SpeedManagerLimitEstimate[last_bad_ups.size()])); } public synchronized SpeedManagerLimitEstimate[] getBadDownloadHistory() { return((SpeedManagerLimitEstimate[])last_bad_downs.toArray(new SpeedManagerLimitEstimate[last_bad_downs.size()])); } protected SpeedManagerLimitEstimate adjustForPersistence( limitEstimate estimate, limitEstimate best_good, limitEstimate last_bad, boolean persistent ) { if ( estimate == null ){ return( null ); } if ( persistent ){ // if result is bad then we return this if ( estimate.getMetricRating() == -1 ){ return( estimate ); } // see if best good/last bad are relevant limitEstimate persistent_limit = null; if ( best_good != null && last_bad != null ){ if ( last_bad.getWhen() > best_good.getWhen()){ persistent_limit = last_bad; }else{ if ( best_good.getBytesPerSec() > last_bad.getBytesPerSec()){ persistent_limit = best_good; }else{ persistent_limit = last_bad; } } }else if ( best_good != null ){ persistent_limit = best_good; }else if ( last_bad != null ){ persistent_limit = last_bad; } if ( persistent_limit == null ){ return( estimate ); } if ( estimate.getBytesPerSec() > persistent_limit.getBytesPerSec()){ return( estimate ); }else{ // need to convert this into a good rating to correspond to the // actual estimate type we have limitEstimate res = estimate.getClone(); res.setBytesPerSec(persistent_limit.getBytesPerSec()); return( res ); } }else{ return( estimate ); } } protected void updateLimitEstimates() { double cm = getCurrentMetricRating(); up_estimate = getEstimatedLimit( true ); if ( up_estimate != null ){ double metric = up_estimate.getMetricRating(); if ( metric == -1 ){ if ( bad_up_in_progress_count == 0 ){ // don't count the duplicates we naturally get when sitting here with a bad limit // and nothing going on to change this situation if ( last_bad_up == null || last_bad_up.getBytesPerSec() != up_estimate.getBytesPerSec()){ bad_up_in_progress_count = BAD_PROGRESS_COUNTDOWN; last_bad_ups.addLast( up_estimate ); if ( last_bad_ups.size() > MAX_BAD_LIMIT_HISTORY ){ last_bad_ups.removeFirst(); } checkCapacityDecrease( true, up_capacity, last_bad_ups ); } } last_bad_up = up_estimate; }else if ( metric == 1 ){ if ( best_good_up == null ){ best_good_up = up_estimate; }else{ if ( best_good_up.getBytesPerSec() < up_estimate.getBytesPerSec()){ best_good_up = up_estimate; } } } if ( bad_up_in_progress_count > 0 ){ if ( cm == -1 ){ bad_up_in_progress_count = BAD_PROGRESS_COUNTDOWN; }else if ( cm == 1 ){ bad_up_in_progress_count--; } } } down_estimate = getEstimatedLimit( false ); if ( down_estimate != null ){ double metric = down_estimate.getMetricRating(); if ( metric == -1 ){ if ( bad_down_in_progress_count == 0 ){ if ( last_bad_down == null || last_bad_down.getBytesPerSec() != down_estimate.getBytesPerSec()){ bad_down_in_progress_count = BAD_PROGRESS_COUNTDOWN; last_bad_downs.addLast( down_estimate ); if ( last_bad_downs.size() > MAX_BAD_LIMIT_HISTORY ){ last_bad_downs.removeFirst(); } checkCapacityDecrease( false, down_capacity, last_bad_downs ); } } last_bad_down = down_estimate; }else if ( metric == 1 ){ if ( best_good_down == null ){ best_good_down = down_estimate; }else{ if ( best_good_down.getBytesPerSec() < down_estimate.getBytesPerSec()){ best_good_down = down_estimate; } } } if ( bad_down_in_progress_count > 0 ){ if ( cm == -1 ){ bad_down_in_progress_count = BAD_PROGRESS_COUNTDOWN; }else if ( cm == 1 ){ bad_down_in_progress_count--; } } } } protected void checkCapacityDecrease( boolean is_up, limitEstimate capacity, LinkedList bads ) { if ( capacity.getEstimateType() == SpeedManagerLimitEstimate.TYPE_MANUAL){ return; } if ( bads.size() < MAX_BAD_LIMIT_HISTORY ){ return; } // remeber, 0 means UNLIMITED!!! int cap = capacity.getBytesPerSec(); // sanity check if ( cap > 0 && cap < 10*1024 ){ return; } List b = new ArrayList( bads ); Collections.sort( b, new Comparator() { public int compare( Object o1, Object o2 ) { limitEstimate l1 = (limitEstimate)o1; limitEstimate l2 = (limitEstimate)o2; return( l1.getBytesPerSec() - l2.getBytesPerSec()); } }); // drop top bottom quarter of measurements int start = MAX_BAD_LIMIT_HISTORY/4; int end = MAX_BAD_LIMIT_HISTORY - start; int total = 0; int num = 0; for (int i=start;i<end;i++){ int s = ((limitEstimate)b.get(i)).getBytesPerSec(); total += s; num++; } int average = total/num; // only consider decreases! if ( cap > 0 && average >= cap ){ log( "Not reducing " + (is_up?"up":"down") + " capacity - average=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( average ) + ",capacity=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( cap )); return; } int total_deviation = 0; for (int i=start;i<end;i++){ int s = ((limitEstimate)b.get(i)).getBytesPerSec(); int deviation = s - average; total_deviation += deviation * deviation; } int deviation = (int)Math.sqrt( ((double)total_deviation) / num ); // adjust if deviation within 50% of capacity if ( cap <= 0 || ( deviation < cap/2 && average < cap )){ log( "Reducing " + (is_up?"up":"down") + " capacity from " + cap + " to " + average + " due to frequent lower chokes (deviation=" + DisplayFormatters.formatByteCountToKiBEtcPerSec(deviation) + ")" ); capacity.setBytesPerSec( average ); capacity.setEstimateType( SpeedManagerLimitEstimate.TYPE_CHOKE_ESTIMATED); // remove the last 1/4 bad stats so we don't reconsider adjusting until more data collected for (int i=0;i<start;i++){ bads.removeFirst(); } }else{ log( "Not reducing " + (is_up?"up":"down") + " capacity - deviation=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( deviation ) + ",capacity=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( cap )); } } protected synchronized limitEstimate getEstimatedLimit( boolean up ) { if ( !variance ){ return( getNullLimit() ); } int num_samples = regions.size(); if ( num_samples == 0 ){ return( getNullLimit()); } Iterator it = regions.iterator(); int max_end = 0; while( it.hasNext()){ region r = (region)it.next(); int end = (up?r.getUploadEndBytesPerSec():r.getDownloadEndBytesPerSec())/SPEED_DIVISOR; if ( end > max_end ){ max_end = end; } } int sample_end = max_end + 1; int[] totals = new int[sample_end]; short[] hits = new short[sample_end]; short[] worst_var_type = new short[sample_end]; ListIterator sample_it = regions.listIterator( 0 ); // flatten out all observations into a single munged metric while( sample_it.hasNext()){ region r = (region)sample_it.next(); int start = (up?r.getUploadStartBytesPerSec():r.getDownloadStartBytesPerSec())/SPEED_DIVISOR; int end = (up?r.getUploadEndBytesPerSec():r.getDownloadEndBytesPerSec())/SPEED_DIVISOR; int metric = r.getMetric(); int weighted_start; int weighted_end; short this_var_type; if ( metric < VARIANCE_GOOD_VALUE ){ // a good variance applies to all speeds up to this one. This means // that previously occuring bad variance will get flattened out by // subsequent good variance weighted_start = 0; weighted_end = end; this_var_type = 0; }else if ( metric < VARIANCE_BAD_VALUE ){ // medium values, treat at face value weighted_start = start; weighted_end = end; this_var_type = VARIANCE_GOOD_VALUE; }else{ // bad ones, treat at face value weighted_start = start; weighted_end = max_end; this_var_type = VARIANCE_BAD_VALUE; } for (int j=weighted_start;j<=weighted_end;j++){ // a bad variance resets totals as we have encountered this after (in time) // the existing data and this is more relevant and replaces any feel good // factor we might have accumulated via prior observations if ( this_var_type == VARIANCE_BAD_VALUE && worst_var_type[j] <= this_var_type ){ totals[j] = 0; hits[j] = 0; worst_var_type[j] = this_var_type; } totals[j] += metric; hits[j]++; } } // now average out values based on history computed above for (int i=0;i<sample_end;i++){ int hit = hits[i]; if ( hit > 0 ){ int average = totals[i]/hit; totals[i] = average; if ( average < VARIANCE_GOOD_VALUE ){ worst_var_type[i] = 0; }else if ( average < VARIANCE_BAD_VALUE ){ worst_var_type[i] = VARIANCE_GOOD_VALUE; }else{ worst_var_type[i] = VARIANCE_BAD_VALUE; } } } // break history up into segments of same speed int last_average = -1; int last_average_change = 0; int last_average_worst_var = 0; int last_max_hits = 0; int worst_var = 0; List segments = new ArrayList(totals.length); for (int i=0;i<sample_end;i++){ int var = worst_var_type[i]; int hit = hits[i]; if ( var > worst_var ){ worst_var = var; } int average = totals[i]; if ( i == 0 ){ last_average = average; }else if ( last_average != average ){ segments.add( new int[]{ last_average, last_average_change*SPEED_DIVISOR, (i-1)*SPEED_DIVISOR, last_average_worst_var, last_max_hits }); last_average = average; last_average_change = i; last_average_worst_var = var; last_max_hits = hit; }else{ last_average_worst_var = Math.max( var, last_average_worst_var ); last_max_hits = Math.max( hit, last_max_hits ); } } if ( last_average_change != sample_end - 1 ){ segments.add( new int[]{ last_average, last_average_change*SPEED_DIVISOR, (sample_end-1)*SPEED_DIVISOR, last_average_worst_var, last_max_hits }); } int[] estimate_seg = null; int estimate_var = 0; // take smallest bad value and largest good if ( worst_var == VARIANCE_BAD_VALUE ){ for (int i=segments.size()-1;i>=0;i-- ){ int[] seg = (int[])segments.get(i); int var = seg[3]; if ( var >= worst_var ){ estimate_seg = seg; estimate_var = var; } } }else{ for (int i=0;i<segments.size();i++){ int[] seg = (int[])segments.get(i); int var = seg[3]; if ( var >= worst_var ){ estimate_seg = seg; estimate_var = var; } } } int estimate_speed; int estimate_hits; if ( estimate_seg == null ){ estimate_speed = -1; estimate_hits = 0; }else{ estimate_speed = -1; if ( worst_var == 0 ){ estimate_speed = estimate_seg[2]; }else if ( worst_var == VARIANCE_GOOD_VALUE ){ estimate_speed = ( estimate_seg[1] + estimate_seg[2])/2; }else{ estimate_speed = estimate_seg[1]; } estimate_hits = estimate_seg[4]; } // override any estimates < 5K to be OK ones as there's little point in recording negative // values lower than this if ( estimate_speed < 5*1024 ){ estimate_var = VARIANCE_GOOD_VALUE; // value of 0 means unlimited if ( estimate_speed <= 0 ){ estimate_speed = 1; } } limitEstimate result = new limitEstimate( estimate_speed, SpeedManagerLimitEstimate.TYPE_ESTIMATED, convertMetricToRating( estimate_var ), estimate_hits, SystemTime.getCurrentTime(), (int[][])segments.toArray(new int[segments.size()][])); return( result ); } public synchronized double getCurrentMetricRating() { if ( ping_count == 0 ){ return( 0 ); } int latest_metric = pings[ping_count-1].getMetric(); if ( variance ){ return( convertMetricToRating( latest_metric )); }else{ return( 0 ); } } public SpeedManagerLimitEstimate getEstimatedUploadCapacityBytesPerSec() { return( up_capacity ); } public void setEstimatedDownloadCapacityBytesPerSec( int bytes_per_sec, float estimate_type ) { if ( down_capacity.getBytesPerSec() != bytes_per_sec || down_capacity.getEstimateType() != estimate_type ){ down_capacity.setBytesPerSec( bytes_per_sec ); down_capacity.setEstimateType( estimate_type ); speed_manager.informDownCapChanged(); } } public SpeedManagerLimitEstimate getEstimatedDownloadCapacityBytesPerSec() { return( down_capacity ); } public void setEstimatedUploadCapacityBytesPerSec( int bytes_per_sec, float estimate_type ) { if ( up_capacity.getBytesPerSec() != bytes_per_sec || up_capacity.getEstimateType() != estimate_type ){ up_capacity.setBytesPerSec( bytes_per_sec ); up_capacity.setEstimateType( estimate_type ); speed_manager.informUpCapChanged(); } } protected synchronized void reset() { setEstimatedDownloadCapacityBytesPerSec( 0, SpeedManagerLimitEstimate.TYPE_UNKNOWN); setEstimatedUploadCapacityBytesPerSec( 0, SpeedManagerLimitEstimate.TYPE_UNKNOWN); ping_count = 0; regions.clear(); last_bad_down = null; last_bad_downs.clear(); last_bad_up = null; last_bad_ups.clear(); saveHistory(); } protected double convertMetricToRating( int metric ) { if ( metric < VARIANCE_GOOD_VALUE ){ return( +1 ); }else if ( metric >= VARIANCE_BAD_VALUE ){ return( -1 ); }else{ double val = 1 - ((double)metric - VARIANCE_GOOD_VALUE )/50; // sanitize if ( val < -1 ){ val = -1; }else if ( val > 1 ){ val = 1; } return( val ); } } protected String getLimitStr( List limits, boolean short_form ) { String str = ""; if ( limits != null ){ Iterator it = limits.iterator(); while( it.hasNext()){ str += (str.length()==0?"":","); limitEstimate l = (limitEstimate)it.next(); if ( short_form ){ str += getShortString( l ); }else{ str += l.getString(); } } } return( str ); } protected String getShortString( SpeedManagerLimitEstimate l ) { return( DisplayFormatters.formatByteCountToKiBEtcPerSec( l.getBytesPerSec())); } protected void generateEvidence( IndentWriter writer ) { writer.println( "up_cap=" + up_capacity.getString()); writer.println( "down_cap=" + down_capacity.getString()); writer.println( "bad_up=" + getLimitStr( last_bad_ups, false )); writer.println( "bad_down=" + getLimitStr( last_bad_downs, false )); if ( best_good_up != null ){ writer.println( "best_up=" + best_good_up.getString()); } if ( best_good_down != null ){ writer.println( "best_down=" + best_good_down.getString()); } } public void destroy() { if ( trans ){ speed_manager.destroy( this ); }else{ Debug.out( "Attempt to destroy non-transient mapper!" ); } } private static class pingValue { private short x; private short y; private short metric; protected pingValue( int _x, int _y, int _m ) { x = (short)_x; y = (short)_y; metric = (short)_m; } protected int getX() { return(((int)(x))&0xffff ); } protected int getY() { return(((int)(y))&0xffff ); } protected int getMetric() { return(((int)(metric))&0xffff ); } protected String getString() { return("x=" + getX()+",y=" + getY() +",m=" + getMetric()); } } private static class region implements SpeedManagerPingZone { private short x1; private short y1; private short x2; private short y2; private short metric; protected region( pingValue p1, pingValue p2 ) { x1 = (short)p1.getX(); y1 = (short)p1.getY(); x2 = (short)p2.getX(); y2 = (short)p2.getY(); if ( x2 < x1 ){ short t = x1; x1 = x2; x2 = t; } if ( y2 < y1 ){ short t = y1; y1 = y2; y2 = t; } metric = (short)((p1.getMetric()+p2.getMetric())/2); } public int getX1() { return( x1 & 0x0000ffff ); } public int getY1() { return( y1 & 0x0000ffff ); } public int getX2() { return( x2 & 0x0000ffff ); } public int getY2() { return( y2 & 0x0000ffff ); } public int getUploadStartBytesPerSec() { return( getX1()*SPEED_DIVISOR ); } public int getUploadEndBytesPerSec() { return( getX2()*SPEED_DIVISOR + (SPEED_DIVISOR-1)); } public int getDownloadStartBytesPerSec() { return( getY1()*SPEED_DIVISOR ); } public int getDownloadEndBytesPerSec() { return( getY2()*SPEED_DIVISOR + (SPEED_DIVISOR-1)); } public int getMetric() { return( metric & 0x0000ffff ); } public String getString() { return( "x="+getX1() + ",y="+getY1()+",w=" + (getX2()-getX1()+1) +",h=" + (getY2()-getY1()+1)); } } private static class limitEstimate implements SpeedManagerLimitEstimate, Cloneable { private int speed; private float estimate_type; private float metric_rating; private long when; private int hits; private int[][] segs; protected limitEstimate( int _speed, double _estimate_type, double _metric_rating, int _hits, long _when, int[][] _segs ) { speed = _speed; estimate_type = (float)_estimate_type; metric_rating = (float)_metric_rating; hits = _hits; when = _when; segs = _segs; // sanitize if ( metric_rating < -1 ){ metric_rating = -1; }else if ( metric_rating > 1 ){ metric_rating = 1; } } public int getBytesPerSec() { return( speed ); } protected void setBytesPerSec( int s ) { speed = s; } public float getEstimateType() { return( estimate_type ); } public void setEstimateType( float et ) { estimate_type = et; } public float getMetricRating() { return( metric_rating ); } protected void setMetricRating( float mr ) { metric_rating = mr; } public int[][] getSegments() { return( segs ); } protected int getHits() { return( hits ); } public long getWhen() { return( when ); } public limitEstimate getClone() { try{ return((limitEstimate)clone()); }catch( Throwable e ){ return( null ); } } public String getString() { return( "speed=" + DisplayFormatters.formatByteCountToKiBEtc( speed )+ ",metric=" + metric_rating + ",segs=" + segs.length + ",hits=" + hits + ",when=" + when ); } } public static void main( String[] args ) { SpeedManagerPingMapperImpl pm = new SpeedManagerPingMapperImpl( null, "test", 100, true, false ); Random rand = new Random(); int[][] phases = { { 50, 0, 100000, 50 }, { 50, 100000, 200000, 200 }, { 50, 50000, 50000, 200 }, { 50, 0, 100000, 50 }, }; for (int i=0;i<phases.length;i++){ int[] phase = phases[i]; System.out.println( "**** phase " + i ); for (int j=0;j<phase[0];j++){ int x_base = phase[1]; int x_var = phase[2]; int r = phase[3]; pm.addPing( x_base + rand.nextInt( x_var ), x_base + rand.nextInt( x_var ), rand.nextInt( r ), false); SpeedManagerLimitEstimate up = pm.getEstimatedUploadLimit( false ); SpeedManagerLimitEstimate down = pm.getEstimatedDownloadLimit( false ); if ( up != null && down != null ){ System.out.println( up.getString() + "," + down.getString()); } } } } }