def analyze(self):
        """Run complete analysis."""
        self._CPU.analyze()
        self._data.analyze()
        self.results = copy.deepcopy(self._CPU.results)
        self.results.update(copy.deepcopy(self._data.results))

        cores_per_numa_domain = self.machine['cores per NUMA domain']

        # Compile total single-core prediction
        self.results['total cycles'] = self._CPU.conv_cy(max(
            self.results['T_OL'],
            sum([self.results['T_nOL']] + [i[1] for i in self.results['cycles']])))
        T_ECM = float(self.results['total cycles']['cy/CL'])

        # T_MEM is the cycles accounted to memory transfers
        T_MEM = self.results['cycles'][-1][1]

        # Simple scaling prediction:
        # Assumptions are:
        #  - bottleneck is always LLC-MEM
        #  - all caches scale with number of cores (bw AND size(WRONG!))

        # Full caching in higher cache level
        self.results['scaling cores'] = float('inf')
        # Not full caching:
        if self.results['cycles'][-1][1] != 0.0:
            # Considering memory bus utilization
            utilization = [0]
            self.results['scaling cores'] = float('inf')
            for c in range(1, cores_per_numa_domain + 1):
                if c * T_MEM > (T_ECM + utilization[c - 1] * (c - 1) * T_MEM / 2):
                    utilization.append(1.0)
                    self.results['scaling cores'] = min(self.results['scaling cores'], c)
                else:
                    utilization.append(c * T_MEM / (T_ECM + utilization[c - 1] * (c - 1) * T_MEM / 2))
            utilization = utilization[1:]

            # Old scaling code
            #self.results['scaling cores'] = (
            #    max(self.results['T_OL'],
            #        self.results['T_nOL'] + sum([c[1] for c in self.results['cycles']])) /
            #    self.results['cycles'][-1][1])
            scaling_predictions = []
            for cores in range(1, self.machine['cores per socket'] + 1):
                scaling = {'cores': cores, 'notes': [], 'performance': None,
                           'in-NUMA performance': None}
                # Detailed scaling:
                if cores <= self.results['scaling cores']:
                    # Is it purely in-cache?
                    innuma_rectp = PrefixedUnit(
                            max(sum([c[1] for c in self.results['cycles']]) + self.results['T_nOL'],
                                self.results['T_OL']) / (T_ECM/T_MEM),
                            "cy/CL")
                    scaling['notes'].append("memory-interface not saturated")
                else:
                    innuma_rectp = PrefixedUnit(self.results['cycles'][-1][1], 'cy/CL')
                    scaling['notes'].append("memory-interface saturated on first NUMA domain")
                # Include NUMA-local performance in results dict
                scaling['in-NUMA performance'] = innuma_rectp

                if 0 < cores <= cores_per_numa_domain:
                    # only in-numa scaling to consider
                    scaling['performance'] = self._CPU.conv_cy(
                        innuma_rectp / utilization[cores - 1])
                    scaling['notes'].append("in-NUMA-domain scaling")
                elif cores <= self.machine['cores per socket'] * self.machine['sockets']:
                    # out-of-numa scaling behavior
                    scaling['performance'] = self._CPU.conv_cy(
                        innuma_rectp * cores_per_numa_domain / cores)
                    scaling['notes'].append("out-of-NUMA-domain scaling")
                else:
                    raise ValueError("Number of cores must be greater than zero and upto the max. "
                                     "number of cores defined by cores per socket and sockets in"
                                     "machine file.")
                scaling_predictions.append(scaling)
        else:
            # pure in-cache performace (perfect scaling)
            scaling_predictions = [
                {'cores': cores, 'notes': ['pure in-cache'],
                 'performance': self._CPU.conv_cy(T_ECM/cores),
                 'in-NUMA performance': self._CPU.conv_cy(T_ECM/cores_per_numa_domain)}
                for cores in range(1, self.machine['cores per socket'] + 1)]

        # Also include prediction for all in-NUMA core counts in results
        self.results['scaling prediction'] = scaling_predictions
        if self._args.cores:
            self.results['multi-core'] = scaling_predictions[self._args.cores - 1]
        else:
            self.results['multi-core'] = None
Run complete analysis.